#include <stdio.h>
const void *ptrprintf = printf;
#pragma section(".exre", execute, read)
__declspec(allocate(".exre")) int main[] =
{
0x646C6890, 0x20680021, 0x68726F57,
0x2C6F6C6C, 0x48000068, 0x24448D65,
0x15FF5002, &ptrprintf, 0xC314C483
};
Предисловие
Итак, начал я с того, что нашел эту статью. Вдохновившись ею, я стал думать, как сделать это на windows.
В той статье вывод на экран был реализован с помощью syscall, но в windows мы сможем использовать только функцию printf. Возможно я ошибаюсь, но ничего иного я так и не нашел.
Набравшись смелости и взяв в руки visual studio я стал пробовать. Не знаю, зачем я так долго возился с тем, чтобы подставлять entry point в настройках компиляции, но как выяснилось позже компилятор visual studio даже не кидает warning если main является массивом, а не функцией.
Основной список проблем, с которыми мне пришлось столкнуться:
1) Массив находится в секции данных и не может быть исполнен
2) В windows нет syscall и вывод нужно реализовать с помощью printf
Поясню чем тут плох вызов функции. Обычно адрес вызова подставляется компилятором из таблицы символов, если я не ошибаюсь. Но у нас ведь обычный массив, где мы сами должны написать адрес.
Решение проблемы «исполняемых данных»
Первая проблема, с которой я столкнулся, ожидаемо оказалось то, что простой массив хранится в секции данных и не может быть исполнен, как код. Но немного покопав stackoverflow и msdn я все же нашел выход. Компилятор visual studio поддерживает препроцессорную директиву section и можно объявить переменную так, чтобы она оказалась в секции с разрешением на исполнение.
Проверив, так ли это, я убедился, что это работает и
#pragma section(".exre", execute, read)
__declspec(allocate(".exre")) char main[] = { 0xC3 };
Немного ассемблера
Теперь, когда я мог исполнять массив нужно было составить код который будет выполняться.
Я решил, что сообщение «Hello, World» я буду хранить в ассемблерном коде. Сразу скажу, что ассемблер я понимаю достаточно плохо, поэтому прошу сильно тапками не кидаться, но критика приветствуется. В понимании того, какой ассемблерный код можно вставить и не вызывать лишних функций мне помог этот ответ на stackoverfow
Я взял notepad++ и с помощью функции plugins->converter->«ASCII -> HEX» получил код символов.
Hello, World!
48656C6C6F2C20576F726C6421
Далее нам нужно разделить по 4 байта и положить на стек в обратном порядке, не забыв перевернуть в little-endian.
48656C6C6F2C20576F726C642100
Делим с конца на 4 байтные hex числа.
00004865 6C6C6F2C 20576F72 6C642100
Переворачиваем в little-endian и меняем порядок на обратный
0x0021646C 0x726F5720 0x2C6F6C6C 0x65480000
Я немного опустил момент с тем, как я пытался напрямую вызывать printf и чтобы сохранить потом этот адрес в массиве. Получилось у меня только сохранив указатель на printf. Позже будет видно почему так.
#include <stdio.h>
const void *ptrprintf = printf;
void main() {
__asm {
push 0x0021646C ; "ld!\0"
push 0x726F5720 ; " Wor"
push 0x2C6F6C6C ; "llo,"
push 0x65480000 ; "\0\0He"
lea eax, [esp+2] ; eax -> "Hello, World!"
push eax ; указатель на начало строки пушим на стек
call ptrprintf ; вызываем printf
add esp, 20 ; чистим стек
}
}
Компилируем и смотрим дизассемблер.
00A8B001 68 6C 64 21 00 push 21646Ch
00A8B006 68 20 57 6F 72 push 726F5720h
00A8B00B 68 6C 6C 6F 2C push 2C6F6C6Ch
00A8B010 68 00 00 48 65 push 65480000h
00A8B015 8D 44 24 02 lea eax,[esp+2]
00A8B019 50 push eax
00A8B01A FF 15 00 90 A8 00 call dword ptr [ptrprintf (0A89000h)]
00A8B020 83 C4 14 add esp,14h
00A8B023 C3 ret
Отсюда нам нужно взять байты кода.
{2} *.*
Начало строк можно убрать с помощью плагина для notepad++ TextFx:
TextFX->«TextFx Tools»->«Delete Line Numbers or First Word», выделив все строки.
После чего у нас уже будет почти готовая последовательность кода для массива.
68 6C 64 21 00 68 20 57 6F 72 68 6C 6C 6F 2C 68 00 00 48 65 8D 44 24 02 50 FF 15 00 90 A8 00 ; После FF 15 следующие 4 байта должны быть адресом вызываемой фунцкии 83 C4 14 C3
Вызов функции с «заранее известным» адресом
Я долго думал, как же можно оставить в готовой последовательности адрес из таблицы функций, если это знает только компилятор. И немного поспрашивав у знакомых программистов и поэкспериментировав я понял, что адрес вызываемой функции можно получить с помощью операции взятия адреса от переменной указателя на функцию. Что я и сделал.
#include <stdio.h>
const void *ptrprintf = printf;
void main()
{
void *funccall = &ptrprintf;
__asm {
call ptrprintf
}
}

Как видно в указателе лежит именно тот самый вызываемый адрес. То, что нужно.
Собираем все вместе
Итак, у нас есть последовательность байт ассемблерного кода, среди которых нам нужно оставить выражение, которое компилятор преобразует в адрес, нужный нам для вызова printf. Адрес у нас 4 байтный(т.к. пишем для код для 32 разрядной платформы), значит и массив должен содержать 4 байтные значения, причем так, чтобы после байт FF 15 у нас шел следующий элемент, куда мы и будем помещать наш адрес.
90 68 6C 64 21 00 68 20 57 6F 72 68 6C 6C 6F 2C 68 00 00 48 65 8D 44 24 02 50 FF 15 00 90 A8 00 ; адрес для вызова printf 83 C4 14 C3
И опять составим 4 байтные значения в little-endian. Для переноса столбцов очень полезно использовать многострочное выделение в notepad++ с комбинацией alt+shift:
646C6890 20680021 68726F57 2C6F6C6C 48000068 24448D65 15FF5002 00000000 ; адрес для вызова printf, далее будет заменен на выражение C314C483
Теперь у нас есть последовательность 4 байтных чисел и адрес для вызова функции printf и мы можем наконец заполнить наш массив main.
#include <stdio.h>
const void *ptrprintf = printf;
#pragma section(".exre", execute, read)
__declspec(allocate(".exre")) int main[] =
{
0x646C6890, 0x20680021, 0x68726F57,
0x2C6F6C6C, 0x48000068, 0x24448D65,
0x15FF5002, &ptrprintf, 0xC314C483
};
Для того чтобы вызывать break point в дебаггере visual studio надо заменить первый элемент массива на 0x646C68CC
Запускаем, смотрим.

Готово!
Заключение
Я извиняюсь если кому-то статья показалась «для самых маленьких». Я постарался максимально подробно описать сам процесс и опустить очевидные вещи. Хотел поделиться собственным опытом такого небольшого исследования. Буду рад если статья окажется кому-то интересной, а возможно и полезной.
Оставлю тут все приведенные ссылки:
Статья «main usually a function»
Описание section на msdn
Некоторое объяснение ассемблерного кода на stackoverflow
И на всякий случай оставлю ссылку на 7z архив с проектом под visual studio 2013
Также не исключаю, что можно было ещё сократить вызов printf и использовать другой код вызова функции, но я не успел исследовать этот вопрос.
Буду рад вашим отзывам и замечаниям.
Комментарии (143)
mayorovp
25.01.2016 18:38+4Вместо заголовочного файла можно было бы просто объявить прототип функции
printf
.ComradeAndrew
25.01.2016 18:53+3Да, вы правы. Об этом я не думал. Посмотрим, если наберется достаточно много интересных поправок, добавлю к статье модифицированный вариант.
Joric
25.01.2016 18:55+1«int printf(char*f,...);» всё равно выходит длиннее "#include <stdio.h>" :)
Joric
25.01.2016 18:59+2Хотя можно без int, линкер сожрёт, но с варнингом. Получится на 1 (!) символ короче.
ComradeAndrew
25.01.2016 19:06+1А у меня даже варнинга не выдает. Если ставить пробел после #include, как все нормальные люди, то получится одинаковая длина.
printf(char*,...); #include <stdio.h>
Варнинг у меня только один. На &ptrprintf. Я хотел добавить макрос с кастом к int, чтобы оставить ту же длину и однородный цвет в таблице массива, но решил оставить как есть.bigfatbrowncat
25.01.2016 19:35+3(ирония)
А если ставить пробел после запятой, как все нормальные люди, то всё равно получится на один символ больше.ComradeAndrew
25.01.2016 19:38+2Тут не поспоришь :)
Но мне кажется, что вариант с прототипом вполне неплохой и поддерживает стиль остального кода.
khim
25.01.2016 19:56+6Зачем вам вообще что-то в скобках? Это же C, не C++!
«int printf();» — законное описание функции, которая принимает «что-то там, неважно что», возврщает int, большего вам и не нужно…ComradeAndrew
25.01.2016 20:02+2Да, действительно. На тулсете 2013 студии компилирует. Однако при компиляции тулсетом v140(visual studio 2015) она кидает error:
error LNK2001: unresolved external symbol _printf
khim
25.01.2016 21:27Скорее всего как-то манглится имя. Посмотрите в обычном «Hello, world!» чего хочет ваш объектник…
xiWera
25.01.2016 18:44-7А еще можно сразу вызывать какойнить 'system' и одним махом столько разных программ «на Си написать» :)
mark_ablov
25.01.2016 18:45+3В Windows тоже можно использовать сисколы.
Дпугое дело, что они version-specific.
Конкретно для вывода на консоль можно использовать NtWriteFile.ComradeAndrew
25.01.2016 19:09+1NtWriteFile в поток вывода? Интересно. Надо попробовать как по длине кода получится.
mayorovp
25.01.2016 20:08+1Плохо по длине кода получится. Потому что дескриптор потока вывода еще получить надо.
sebres
25.01.2016 20:54+1Ну есть жеж WriteConsoleA…
handle на stdout получается через GetStdHandle(-11)push -11 call GetStdHandle
под спойлером пример для nasm+linkextern _ExitProcess@4, _GetStdHandle@4, _WriteConsoleA@20 %define ExitProcess _ExitProcess@4 %define GetStdHandle _GetStdHandle@4 %define WriteConsoleA _WriteConsoleA@20 global _main section .data msg db "Hello World!", 13, 10, 0 msg.len equ $ - msg section .text _main: push -11 call GetStdHandle push 0 push 0 push msg.len push msg push eax call WriteConsoleA push 0 call ExitProcess
grechnik
25.01.2016 22:24+5Для консоли это не будет работать, у консоли вообще нет ядерного хэндла. WriteFile, обнаружив kernel32-хэндл консоли, вызывает WriteConsole, WriteConsole работает в конечном счёте через RPC к модулю winsrv.dll в процессе csrss. (В частности, из-за такой схемы в XP консольные окна не подхватывали темы оформления — грузить абы что в системный процесс csrss MS не хотела. В Vista для этого в цепочку добавили ещё один процесс conhost.exe, вызываемый из csrss).
mark_ablov
25.01.2016 19:42+1Как-то так. Разве что 0x1A0007 я глянул у себя в системе (8.1), для других версий винды оно другое.
__asm { xor edi, edi push 0x0021646C push 0x726F5720 push 0x2C6F6C6C push 0x65480000 lea eax, [esp + 2] push edi push edi mov ebx, esp push 13 push eax push ebx ; output pointer to IoStatusBlock, does not matter if we overrite some data in stack push edi push edi push edi ; get output file handler from TEB/PEB mov eax, fs:[030h] mov eax, [eax+10h] mov eax, [eax+1Ch] push eax push 0 ; ret addr, skipped by syscall handler ; call func mov eax, 1A0007h ; NtWriteFile, check syscall # for your windows version call fs:[0C0h] add esp, 38h ; 10h string + 24h syscall stack + 4h ret }
ComradeAndrew
25.01.2016 20:03+1Честно говоря совсем не понял для чего столько пушей и что в итоге получается. Можете немного подробнее объяснить как это работает?
saferif
25.01.2016 21:20Много пушей, поскольку параметров у функции много. Правда многие равны NULL. Описание функции по ссылке.
NTWriteFile, он же ZwWriteFileNTSTATUS ZwWriteFile( _In_ HANDLE FileHandle, _In_opt_ HANDLE Event, _In_opt_ PIO_APC_ROUTINE ApcRoutine, _In_opt_ PVOID ApcContext, _Out_ PIO_STATUS_BLOCK IoStatusBlock, _In_ PVOID Buffer, _In_ ULONG Length, _In_opt_ PLARGE_INTEGER ByteOffset, _In_opt_ PULONG Key );
JKornev
25.01.2016 21:24-3mark_ablov решил выпендрится и переписал вашу реализацию hello world включив в неё функциональность вывода на консоль базонезависимым кодом. Однако реализация эта будет работать скорее всего только на некоторых системах 8.1 x64
MacIn
25.01.2016 22:44В принципе, туда еще небольшой шажок — поиск номера функции в теле NtWriteFile и будет веселее.
JKornev
26.01.2016 00:48Мне кажется лучше было бы просто узнать адрес NtWriteFile через PEB_LDR_DATA и вызвать ф-ю напрямую, потому что зная номер сервиса нам ещё нужно как-то попасть в ядро, а в разных разрядностях это реализовано по-разному
MacIn
26.01.2016 02:32+1Что разрядности — в разных билдах номер может отличаться. Да, и как выяснилось ниже, NtWriteFile не катит — он не работает с псевдо-handle'ами. Надо через LDR_DATA вытаскивать WriteFile или сразу WriteConsole.
JKornev
26.01.2016 13:16Дело не в номерах, а в особенностях механизмов входа в ядро для x86 и x64(WOW64). Во-первых они отличаются, во-вторых если мне не изменяет память для x64 необходимо класть параметры с учётом того что они будут обрабатываться 64-битным кодом.
Так что да, лучше юзать что-то высокоуровневое
MacIn
26.01.2016 15:32Мы так и так компилируем 32 разрядный код в примере выше + стандартная точка входа из TEB. А вот номера сервисов отличаются от SP к SP, например.
JKornev
26.01.2016 15:42call fs:[0C0h] в Windows Vista и выше не будет работать для x86, там используется SharedUserData->SystemCallStub
grechnik
25.01.2016 22:29И что, правда работает с выводом на консоль (если не перенаправлять вывод в файл)?
MacIn
25.01.2016 22:41Отчего нет — по 1Ch лежит HANDLE StdOutput.
grechnik
25.01.2016 22:49Хэндлы бывают разные. Вы пробовали запустить код от mark_ablov?
#include <stdio.h> #include <intrin.h> int main() { printf("%08x\n", ((int**)__readfsdword(0x30))[0x10/4][0x1C/4]); return 0; }
Win7 WOW64:
C:\> test | more 0000006C C:\> test 0000000F
Первый запуск — перенаправление вывода, 0x6C — нормальный ядерный хэндл. Второй запуск — вывод напрямую на консоль, хэндл, как легко видеть, ненормальный (ядерные хэндлы всегда делятся на 4).MacIn
25.01.2016 23:03+1Это псевдо-хэндл. У меня на ХР выдает 7 (кстати, GetStdHandle делает именно это — роется в TEB/PEB/EPROCESS_PARAMETERS), и это рабочий handle, по крайней мере WriteFile его принимает (Nt(Zw)WriteFile не пробовал).
upd: понятно, WriteFile обрабатывает это особо, а ntdll не примет. Тогда да, вы правы.
mark_ablov
26.01.2016 06:58Вполне возможно, да.
Проверял из ms vc для простоты. Тогда можно RPC слать серверу консолей. Что малость сложнее, но тоже ничего невозможного.
mark_ablov
26.01.2016 06:53+2grechnik
26.01.2016 14:50+1Хм, интересно. Win8.1, говорите?
На Win7 ожидаемо фейлится с STATUS_OBJECT_TYPE_MISMATCH:
Картинкаmark_ablov
26.01.2016 14:57> WriteFile не делает специальных проверок и просто вызывает NtWriteFile
Угу, я когда смотрел как она преобразовывает STD_OUTPUT_HANDLER не видел никаких вызовов WriteConsole, а просто обращение к данным PEBа.
ababo
25.01.2016 19:53+13Чрезвычайно простой и практичный подход к написанию ПО. Ждём более сложные примеры.
ComradeAndrew
25.01.2016 20:57+2Если правда интересно, то постараюсь придумать что-то похожее. Пока времени мало, но как сдам сессию наверняка вернусь к этому. Я даже не ожидал, что будет такой интерес :)
Eivind
25.01.2016 20:37+6А еще можно попробовать переписать main «на лету»:
#include <stdint.h> #include <stdio.h> #include <sys/mman.h> #include <unistd.h> int main() { uintptr_t offset = 0x71; size_t pagesize = sysconf( _SC_PAGESIZE ); char* m = (char*)&main; uintptr_t pagestart = ( (uintptr_t)m ) & -pagesize; mprotect( (void*)pagestart, ( (uintptr_t)m ) - pagestart + offset, PROT_READ | PROT_WRITE | PROT_EXEC ); m[offset] = 'e'; printf( "H%cllo World!\n", 'a' ); }
leremin
25.01.2016 21:34+3Когда вижу подобный код в голове проскакивает непременное «За изобретение ставлю пять, а… по предмету — неуд». Это все очень хорошо и интересно, но практическое применение этого очень редко оправдано. Нисколько вас не упрекаю, если могло так показаться.
ComradeAndrew
25.01.2016 21:41+6Я абсолютно с вами согласен. Это исключительный интерес. И в первую очередь проба себя в низкоуровневых областях программирования, если можно так выразиться.
klirichek
28.01.2016 20:53Всё от задачи зависит.
Если вы пишете hello world — это одно. А если антивирус — всё уже гораздо ближе к ненормальному.
Во времена XP писал плагин для Outlook Express — так там чтобы заюзать его собственную функцию, показывающую диалог выбора папок для писем и возвращающую результат делал страшный костыль: ставил хук на создание окна; затем имитировал команду меню приложения «перейти в папку» (которая как раз приводила к вызову искомой функции), в хуке ловил появляющееся окно, дальше брал адрес локальной переменной (которая, разумеется, создаётся на стеке) и от этого адреса раскручивал вверх память, проверяя каждое слово, не является ли оно адресом из исполнимой части экзешника процесса (для этого сперва парсил заголовки PE, чтобы понять, в каком адресном диапазоне находится именно его код). Причём сделать это нужно было дважды: первый раз под эти условия попадает адрес возврата из оконной функции (тот, что лежит на стеке) а следующий — адрес возврата из искомой функции выбора диалога уже в недрах экзешника. После этого в нужном адресе отступаем назад на код команды call, и из этого машкода выцарапываем смещение нужной функции. Вычисляем адрес, проверяем, что он попадает в секцию relocation экзешника — и тогда да, это то, что нужно.
Так вот этот «костыль» успешно пошёл в продакшн; успешно взлетел на 64-битной винде; успешно взлетел позже на winmail (когда пришла vista), и насколько мне известно, пошёл «в тираж» и далее.
Так что не всякое «ненормальное программирование» даёт хрупкий и непредсказуемый результат.
ateraefectus
25.01.2016 21:59+2За изобретение ставлю пять, а… по предмету — неуд
Тот самый троллейбус.jpg =) (Против статьи ничего не имею, если что)
Wedmer
25.01.2016 22:37+1На самом деле неплохой прием для обфускации кода.
ComradeAndrew
25.01.2016 22:40+8Ну разве что исходного. Дизассеблер будет выглядеть как код составленный заботливым разработчиком (:
Wedmer
26.01.2016 00:22+1Против дизассемблирования есть другие приемы) Данный метод обфускации хорош при работе с недобросовестным заказчиком, если вдруг по договору исходный код должен передаваться. Обычно такие дяди имеют на борту студента, который может собрать софтину и проанализировать код на закладки типа «Демонстрационная Версия». Конечно подобные договора не стоит заключать, но иногда выхода нет (начальство недальновидное, принципиальная необходимость выполнить эти работы, госзаказ и т.д. и т.п.).
khim
26.01.2016 01:27+4Конечно подобные договора не стоит заключать, но иногда выхода нет
Либо я ничего не понимаю, либо одно из двух.
Не знаю как там насчёт «нодобросовестного заказчика», но передача подобных кодов — это уж точно «недобросовестный исполнитель».Wedmer
26.01.2016 03:30Критерием «недобросовестного исполнителя» является передача частично обфусцированного кода или факт самой передачи кода?
khim
26.01.2016 16:32+1Сама передача кода мне кажется вообще «дефолтным» поведением (зачем мне компонент без исходников?), хотя, конечно, она должна оговариваться в договоре. А вот передача обфусцированного кода — это уже повод для того, чтобы разорвать отношения и больше никогда к подобному поставщику не обращаться.
Причём независимо от того, будут ли в принципе выданы коды лучше компонент, полученный от такого поставщика переписать при первой возможности: всех оставленныъ лазеек простым просмотром не выявить, а если разбираться досконально и вычитывать каждую строчку — так можно и с нуля всё сделать примерно за те же деньги.
Другое дело если вам передают компонент не специально для вас разработанный, а такой, в который соответствующая компания вложила годы разработки. Тут нужно явно оговорить, что, наоборот, передаются только бинарники, исходники не передаются. А переписывать ли такой компонент — нужно судить по бизнес-обстановке. Если он вам нужен «всерьёз и надолго» (то есть на нём ваш бизнес «висеть» будет) — лучше переписать.
P.S. Я, разумеется, говорю про IT компании и случай когда вас же поставщик может решить обойтись без вас в качестве посредника. Конечно переписывать системы складского учёта я не предлагаю (представить себе что фирма по внедрению 1С начнёт вдруг «грузить апельсины бочками» достаточно сложно).Wedmer
26.01.2016 17:47+1Когда ваш исходный код окажется в третьей конторе, и она волшебным образом получит этот проект на исполнение после разрыва договора с вами… Я всего лишь предположил, что в случаях крайней необходимости можно этим пользоваться. Ведь проведение подобной обфускации крайне затратное дело. Лично я считаю, что с мутными товарищами, что заказчиками, что подрядчиками, лучше дел не иметь. Но директорату, как говорится, виднее. Бывает ещё так, что заказчик навязывает вам субподрядчика, который ничего не делает, но, внезапно, должен иметь доступ к исходному коду.
monah_tuk
26.01.2016 07:58+2Практические знания подобного рода становятся очень сильно востребованными, когда, например, поднимаешь тулчейн для новой embedded железки: писать так не нужно, но понимать, что происходит под капотом — это просто необходимо. Особенно когда за байты война начинается.
Nomad1
26.01.2016 00:22На мой взгляд, экзешник большой выходит. С /nodefaultlib и некоторым колдунством можно получить чуть больше исходник, но ужаться до ~500 байт.
Как-то так#pragma comment(linker, "/NODEFAULTLIB") #pragma comment(linker, "/FILEALIGN:16") #pragma comment(linker, "/ALIGN:16")// Merge sections #pragma comment(linker, "/MERGE:.rdata=.data") #pragma comment(linker, "/MERGE:.text=.data") #pragma comment(linker, "/MERGE:.reloc=.data") // Favour small code #pragma optimize("gsy", on)
grechnik
26.01.2016 00:31#pragma comment(linker, "/FILEALIGN:16") #pragma comment(linker, "/ALIGN:16")// Merge sections
… и получить файл, не работающий на x64.Nomad1
26.01.2016 00:35Ну так это ж «ненормальное программирование» :) Я ж не говорю о том, что исходник и так на других ОС, компиляторах и архитектурах не работает
JKornev
26.01.2016 01:52Интересно почему не рабочий?
grechnik
26.01.2016 02:57+1А это у MS спрашивать надо. Если 32-битный бинарник собран с выравниванием меньше размера страницы (и некоторыми дополнительными ограничениями), то 32-битная система его загрузит, а 64-битная — нет.
JKornev
26.01.2016 13:08Просто насколько я знаю минимально допустимое выравнивание для PE это file:0x200, virtual:0x1000, у вас есть пруф бинарника с более низким выравниванием?
grechnik
26.01.2016 15:01+1Возьмите любой драйвер.
Если настаиваете именно на exe-шнике — вот банальный MessageBox с FileAlignment = SectionAlignment = 0x20: yadi.sk/d/385e5Lhqnkybi. На 32-битной XP точно работает.
PapaBubaDiop
26.01.2016 00:34Стесняюсь спросить, а всякие прерывания INT 21h и более системные (15 что-ли) для вывода символов на терминал уже не работают? Раньше можно покороче сделать было байт-код.
ComradeAndrew
26.01.2016 00:40Раньше можно покороче сделать было байт-код.
Где это работало?
На сколько я знаю в windows прерывания заблокированы начиная с windows 2000.MacIn
26.01.2016 00:51+3Не совсем. Вплоть до XP SP2(?) syscall идет через int 2Eh…
но мы понимаем, что вопрос выше чуть о другом ;)
MacIn
26.01.2016 00:50Если вы соберете под MS-DOS чистый MZ exeшник, то будет работать, и то не во всех системах.
imwode
26.01.2016 03:56Я тне пргограммист, поэтому спрошу прямо — во что дизассемблер разберет сгенерированный из этой рпограммы машинный код?
MacIn
26.01.2016 04:22Собственно это указано ниже «Компилируем и смотрим дизассемблер.»
Ну, плюс секция импорта и т.п.imwode
26.01.2016 04:28Ой, а я почему-то думал, что дизассемблер в фугкции собирает, типа программу на си выдает, которая компилируется в такой же ассемблерный код
Я ассемблер не знаю, собственно вопрос в том — какой «безхитростный» код даст аналогичный ассемблерный листинг? С нормальным мейном-функцией, принтэфом и строкой в параметре или что-то другое?
Т.е. вопрос собственно в том, с чем автор в итоге поигрался — только с кодом или с кодом и ассемблером или с конечной программой тоже?MacIn
26.01.2016 04:48+3Ой, а я почему-то думал, что дизассемблер в фугкции собирает, типа программу на си выдает, которая компилируется в такой же ассемблерный код
Это называется декомпилятор.
с чем автор в итоге поигрался
Да ни с чем. Написал на ассемблере вызов printf, выписал шестнадцатеричные коды и вставил. Поиграв в ассемблер (программу). Эквивалентно обычному printf(«Hello world»), только написано с подвывертом.
Например, так же в старые времена в Бейсике работали с мышью:
MouseData: DATA 55,89,E5,8B,5E,0C,8B,07,50,8B,5E,0A,8B,07,50,8B DATA 5E,08,8B,0F,8B,5E,06,8B,17,5B,58,1E,07,CD,33,53 DATA 8B,5E,0C,89,07,58,8B,5E,0A,89,07,8B,5E,08,89,0F DATA 8B,5E,06,89,17,5D,CA,08,00 ... SUB MouseDriver (Ax, Bx, Cx, Dx) DEF SEG = VARSEG(Mouse$) Mouse = SADD(Mouse$) CALL Absolute(Ax, Bx, Cx, Dx, Mouse) END SUB
ComradeAndrew
26.01.2016 14:53В этом комментарии я показал во что превращается main. В то же самое, что и вставили в массив, компилятор никак не изменил этот код, потому что для него это были данные, а не код.
rprokop
26.01.2016 14:17+2Как собрать рабочий экзешник на Visual Studio вообще без исходника:
link /OUT:dummy.exe /SUBSYSTEM:CONSOLE /ENTRY:ExitProcess@4 kernel32.lib
http://rsdn.ru/forum/humour/3748806.all
jacob1237
26.01.2016 14:22Автор, а куда в исполняемом файле компилятор в итоге втыкает EntryPoint — в Вашу секцию .exre? Или он в итоге в .code подставляет переход (jmp) к .exre?
ComradeAndrew
26.01.2016 14:51+1Точно так же, как если бы main был функцией. main является EntryPoint по умолчанию.
ВызываетсяmainCRTStartupMacIn
26.01.2016 15:38+1Это можно указать прямо:
link.exe /ALIGN:16 /FILEALIGN:16 /FORCE:UNRESOLVED /SUBSYSTEM:WINDOWS /SECTION:.text,ERW /MERGE:.rdata=.text /MERGE:_INIT_=.text /MERGE:_EXIT_=.text /ENTRY:Start$qqsuiuiui Hello.obj system.obj kernel32.lib user32.lib /out:Hello.exeComradeAndrew
26.01.2016 17:55/SECTION:.text,ERW
Это что, устанавливает в секции все атрибуты? Ого.MacIn
26.01.2016 18:48Угу, потом сливаются все секции в одну, изгоняется на мороз DOS stub и получается маленький файл.
jacob1237
26.01.2016 15:19Прошу прощения, перепутал секцию .code и .text)
В общем понятно, компилятор-таки городит городули с JMP к новой секции.
Просто технически ничего не мешает поменять адрес EntryPoint сразу на .exre, чтобы избежать лишних переходов.
А сможете теперь заставить компилятор переходить сразу к выполнению .exre? Беру Вас на «слабо»! =)
И у Вас там, похоже, еще и рантайм вкомпиливается, судя по вызовам __crtUnhandledException?ComradeAndrew
26.01.2016 17:58Я думал об этом, ещё до того как написал этот «Hello, World!», но так и не осилили. На данный момент я плохо знаю структуру exe файла и тонкости компиляторов.
И да, в том экзешнике куча рантайма и дебага.jacob1237
26.01.2016 18:49+1Почитайте про структуру не exe файла, а формата PE в целом (Portable Executable). Что DLL, что EXE суть один и тот же формат.
Ничего сложного нет, и кстати формат ELF в Linux схематично очень похож на PE, так что в любом случае будет полезно)
Вот вроде бы неплохо структурированная информация: cs.usu.edu.ru/docs/pe
Раньше это все было на wasm.ru, но сейчас у них так какие-то обновления происходят.ComradeAndrew
26.01.2016 21:20Спасибо, вы кстати. Не так давно искал где бы понятнее и подробнее можно об этом почитать.
tyomitch
26.01.2016 21:04Так а ничего мудрёного же нет.
В свойствах проекта, в дереве «Configuration Properties -> Linker -> Advanced», первый пункт «Entry Point» установить в «main».
Всё.
Теперь из проекта компилируется экзешник размером 2560 байт — без рантайма, без __tmainCRTStartup, без __crtUnhandledException, и даже без секции .text.
Ещё можно отключить внедрение манифеста, тогда экзешник будет 1536 байт.
И всё это — стандартными средствами Студии, безо всякого шаманства с заголовками PE :-)ComradeAndrew
26.01.2016 21:19И правда ведь. Неужели я так криво пробовал настройки, что решил, что это не так работает. Спасибо, много полезного на хабре узнать можно.
И еще, не подскажете ли такую вещь: в «Entry Point» можно любую точку входа вставить или только определенные? Где-то прочел, что линковщик ожидает определенные.
tyomitch
26.01.2016 16:19+3Ну-ка, братцы, а вот так работает?
Без printf и без сисколов! Только массив и больше ничего!
Ищет в памяти kernel32, и потом у него в таблице экспорта ищет WriteFile:
Проверял на Windows 7.#pragma section(".exre", execute, read) __declspec(allocate(".exre")) int main[] = { 0x0a0d2168, 0x726f6800, 0x6f68646c, 0x6857202c, 0x6c6c6548, 0x6a54006a, 0x0483540d, 0xd2330c24, 0x30528b64, 0xff10428b, 0x8bfc1c70, 0x528b0c52, 0x28728b14, 0x000018b9, 0x33ff3300, 0x613cacc0, 0x202c027c, 0x030dcfc1, 0x8bf2e2f8, 0xff81105a, 0x6a4abc5b, 0xd975128b, 0x8b555756, 0x6c8b3c53, 0xeb037813, 0x8b184d8b, 0xfb03207d, 0x8b4933e3, 0xf3038f34, 0xd233c033, 0x74c43aac, 0x0dcac107, 0xf4ebd003, 0x791ffa81, 0xe075e80a, 0x0324558b, 0x0c8b66d3, 0x1c558b4a, 0x048bd303, 0xebc3038a, 0x5f5dcc01, 0x83d0ff5e, 0xc310c4};
MacIn
26.01.2016 16:34По ординалу?
tyomitch
26.01.2016 17:52Нет, по имени.
MacIn
26.01.2016 18:47Просто лень в дизасм загонять — код короткий, вряд ли полноценно по имени ищет, или там упрощенное сравнение?
grechnik
26.01.2016 20:05+1Там сравнение хэша от имени с магической константой.
tyomitch
26.01.2016 21:07Да, так и есть.
Причём код для побайтного сравнения имени функции был бы ещё короче: благо,repe cmpsb
занимает всего два байта.
MacIn
26.01.2016 23:20Как раз о хэше и думал, стандартный ход.
tyomitch
27.01.2016 01:25+1Я на первопроходство и не претендовал; но длина кода тут совершенно ни при чём. Вот код с побайтовым сравнением, он даже на три байта короче вышел.
#pragma section(".exre", execute, read) __declspec(allocate(".exre")) int main[] = { 0x0a0d2168, 0x726f6800, 0x6f68646c, 0x6857202c, 0x6c6c6548, 0x6a54006a, 0x0483540d, 0xd2330c24, 0x30528b64, 0xff10428b, 0x8bfc1c70, 0x528b0c52, 0x28728b14, 0x000018b9, 0x33ff3300, 0x613cacc0, 0x202c027c, 0x030dcfc1, 0x8bf2e2f8, 0xff81105a, 0x6a4abc5b, 0xd975128b, 0x6a555756, 0x46656865, 0x57686c69, 0x8b746972, 0x6c8b3c53, 0xeb037813, 0x8b18558b, 0xc3032045, 0x90348b4a, 0x0ab9f303, 0x8b000000, 0x75a6f3fc, 0x24458bef, 0x8b66c303, 0x458b500c, 0x8bc3031c, 0xc3039004, 0x5d0cc483, 0xd0ff5e5f, 0xc310c483 };
MacIn
27.01.2016 04:47Просто хэш-функции разные бывают. Это может быть и CRC32, что солидно больше места занимает.
tyomitch
27.01.2016 12:19Предлагаю вам взять в руки Студию и реализовать тот же самый код ещё короче, с использованием любой хэш-функции по вашему вкусу :-)
MacIn
27.01.2016 17:16Зачем? Мне безразлична длина вашего кода. Я просто высказал предположение, на глаз, что судя по длине, используется простая хэш-функция. А вы уже соревнование затеваете, будто я претензии к длине кода предъявляю :) Я вчера уже ужимал HW exeшник с обычными импортами до минимума, так что свое спортивное любопытство уже удовлетворил (меньше 1к). К тому же на Си я почти не пишу.
И, если честно, мне просто лень переводить ваш массив обратно в код, иначе бы я не спросил про способ поиска.tyomitch
27.01.2016 17:29Я просто высказал предположение, на глаз, что судя по длине, используется простая хэш-функция.
Вы совершенны правы в том, что используется простая хэш-функция.
Но я на протяжении всей ветки комментариев пытаюсь объяснить, что такой вывод не стоило делать на основании длины кода, потому что и без использования хэш-функции код длиннее не становится.MacIn
27.01.2016 17:44+1Нет-нет, мой ход мыслей был таков «для сложного хеша кода маловато, наверно что-то простое. Ну вряд ли же это просто сравнение». Т.е. обычное сравнение почти не рассматривалось. Я просто плохо подобрал слова в вопросе «код короткий, вряд ли полноценно по имени ищет, или там упрощенное сравнение?» здесь и имелось в виду, что вряд ли сравнение — это неспортивно, наверно короткая х.ф.
Кстати, сколько ваш итоговый бинарник весит?tyomitch
27.01.2016 18:101536 байт после отключения рантайма и манифеста.
Если ещё и отладочную информацию отключить, остаётся ровно килобайт.
Дальше, думаю, встроенными средствами Студии не ужать, придётся ковыряться в бинарнике руками.
tyomitch
27.01.2016 18:17Ну собственно, оказалось достаточно стереть все нули из конца файла и поменять SizeOfRawData в заголовке секции, чтобы остался работоспособный экзешник в 680 байт.
(У меня система x64, на ней /FILEALIGN:16 не запускается.)
А у вас сколько байт было в итоге?MacIn
27.01.2016 18:26816 после линкреа без копания руками, но с выравниванием по 16. Можно ужать еще за счет заголовков.
Ага, 716 с одной секцией. Еще 256 dosstub а надо извлечь.tyomitch
27.01.2016 19:56Чтобы запускалось на x86, /FILEALIGN должен быть как минимум 512, т.е. все заголовки вместе взятые меньше чем 512 байт занимать не могут.
Активным «копанием руками в бинарнике» мне удалось уменьшить свой файл до 653 байт:base64TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAADdHerZmXyEipl8hIqZfISKhy4Niph8hIqHLhWKmHyEilJpY2iZfISKAAAAAAAAAABQRQAATAEBAPLwqFYAAAAAAAAAAOAAAgELAQkAAAAAAAACAAAAAAAAABAAAAAQAAAAEAAAAABAAAAQAAAAAgAABQAAAAAAAAAFAAAAAAAAAAAgAAAAAgAAAAAAAAMAQIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5leHJlAAAAkAAAAAAQAACFAAAAAAIAAAAAAAAAAAAAAAAAAEAAAGBIZWxsbywgV29ybGQhDQoAV3JpdGVGaWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqAFRqD+gAAAAAgSwkOg4AADPSZItSMItCEP9wHP90JASDBCQQ/ItSDItSFItyKLkYAAAAM/8zwKw8YXwCLCDBzw0D+OLyi1oQgf9bvEpqixJ12YtTPItsE3gD64tVGItFIAPDSos0kAPzuQoAAACLPCTzpnXui0UkA8NmiwxQi0UcA8OLBJADw1v/0MM=MacIn
27.01.2016 20:15Чтобы запускалось на x86, /FILEALIGN должен быть как минимум 512, т.е. все заголовки вместе взятые меньше чем 512 байт занимать не могут.
Минимальный FILEALIGN — 16. Я довел до 640 и бросил, после работы покопаюсь еще.tyomitch
27.01.2016 20:43Да, извиняюсь, я имел в виду «Чтобы запускалось на x64, /FILEALIGN должен быть как минимум 512». Комментаторы выше это отмечали тоже.
Я тем временем додавил свой экзешник до 365 байт, и он у меня по-прежнему запускается на x64 :-)
Покажу потом, чтобы спортивный интерес не отбивать.MacIn
27.01.2016 20:58365 с импортами? Куда далее идти и так понятно — надо схлопывать MZ, PE и Optional Header.
tyomitch
27.01.2016 21:02Без «настоящих» импортов — точно так же ищет в памяти нужные модуль и функцию, как раньше.
MacIn
27.01.2016 21:42А, ну я-то модуль с секцией импорта ужимаю. «Так» можно, конечно, еще меньше. Мне сильно мешает вкомпилированный код для SEH цепочки. Если дальше жать, это будет уже малость нечестно, потому что я покромсаю то, что создал компилятор.
Сейчас у меня 640, если выкинуть «лишний» код для SEH, можно примерно 64 байта выиграть. Потом заголовки, но проблема в том, что для импорта нужен optional header.tyomitch
27.01.2016 21:58Optional header, несмотря на название, обязателен в любом случае — например, точка входа записывается именно там.
Но я в своём файле нисколько не стеснялся кромсать всё то, что создал компилятор :-)grechnik
29.01.2016 13:48+1Теоретически минимальный размер файла, способного запускаться на x64, — 268 байт (
4 + sizeof(IMAGE_NT_HEADERS64)
). И если «не стесняться кромсать всё, что создал компилятор» и вообще делать PE-файл руками, то такой файл сделать можно: code.google.com/archive/p/corkami/wikis/PE.wiki, «universal tiny PE» (там есть ссылка «nightly builds», ведущая на dropbox, в котором есть архив pe.rar, в котором есть файл tiny.exe). Он выводит строку " * 268b universal tiny PE" вместо «Hello, World!».tyomitch
29.01.2016 14:00Да, я уже нашёл этот пример.
Я правильно понимаю, что он не запустится на системах с принудительным DEP?grechnik
29.01.2016 14:10+1Теоретически — должен. Если SectionAlignment < PageSize, то ядро маппит вообще всё, начиная с заголовка, как PAGE_EXECUTE_READWRITE. 32-битный загрузчик из WOW64-подсистемы потом пытается исправить атрибуты секций (из-за чего файлы, просто собранные с /align:16, и падают), но не трогает заголовок.
Практически — не проверял.tyomitch
29.01.2016 15:42Проверил. Работает. Поразительно.
Но с принудительным ASLR всё-таки валится.
— Ага-а-а! — с облегчением сказали суровые сибирские лесорубы, и ушли валить лес топорами.
MacIn
29.01.2016 17:06Для 32 еще меньше, 133 байта.
grechnik
29.01.2016 17:15+1Это для совсем старых систем, до какого-то сервиспака XP. На XP SP3 и позже уже минимум 252 байта (
4 + sizeof(IMAGE_NT_HEADERS32)
).MacIn
29.01.2016 17:17Нет, холостой 133 загружается без проблем. По крайней мере «not a valid win32 application» нет.
grechnik
29.01.2016 17:25Как MZ-stub, ага. В Process Monitor посмотрите — ntvdm.exe там запускается.
MacIn
29.01.2016 17:26Нет, PE — импорт kernel32 есть
grechnik
29.01.2016 17:28ntvdm.exe тоже загружает kernel32.
MacIn
29.01.2016 17:37Нет, нет. PE файл, прогнал через WinDbg.
start end module name 00400000 00400104 image00400000 C (no symbols) 7c800000 7c8f6000 kernel32 (deferred) 7c900000 7c9b2000 ntdll (pdb symbols)
Это все модули, во время BP около точки входа.grechnik
29.01.2016 17:40> 00400000 00400104
Выглядит как 260 байт.MacIn
29.01.2016 17:42А файл — внезапно — 133.
grechnik
29.01.2016 17:47На XP SP3?
У меня на руках виртуалка с ntoskrnl.exe версии 5.1.2600.5512, в ней подобное не проходит.MacIn
29.01.2016 17:53Ага, на нем, родимом, сижу. Файл вообще веселый — file§ion align — 4, размер OptionalHeader — 4, MZ тоже по сути только 4 байта. И оно работает.
Хех, & sect преобразовалось в параграф.
5.1.2600.6419
grechnik
29.01.2016 18:47Забавно. Если файл есть в кэше данных — может быть короче 252 байт. Если нет (тот же самый файл на сетевом диске без дополнительных условий, или тот же самый файл на свежевставленной флешке либо после перезагрузки системы при прямой загрузке в WinDbg) — будет ntvdm.
В семёрке шизофрению пофиксили, там не грузится консистентно.MacIn
29.01.2016 19:07И вправду, забавно. Но так или иначе, 133 загрузить можно B), пусть только в SP3.
rprokop
26.01.2016 16:58На XP, кстати экзешник не запускается, если ничего не импортируешь из kernel32.dll (прямо или через другие dll).
ComradeAndrew
26.01.2016 19:05Круто! Работает на win10. А вы сразу в ассемблере писали или на си код есть?
tyomitch
26.01.2016 19:22+1Сразу в ассемблере; но большую часть кода не писал сам, а утянул из гуляющих по интернету листингов, которые легко гуглятся по использованным в них «магическим значениям».
bigfatbrowncat
Статья действительно «для самых маленьких» и это — отлично. Прекрасный способ «порвать шаблон» людям, которые, немного попрограммировав на каком-то языке (например, Си), не понимают, как работает компилятор и что на самом деле получается на выходе. Одно дело зазубрить «компилятор превращает исходный текст программы в машииный код», и совсем другое — иллюстрированно и по шагам проделать это самому и увидеть, что получилось.
К вашей статье можно отсылать людей, которые думают, что на современных компьютерах больше нельзя программировать в кодах :)