Comments 143
Статья действительно «для самых маленьких» и это — отлично. Прекрасный способ «порвать шаблон» людям, которые, немного попрограммировав на каком-то языке (например, Си), не понимают, как работает компилятор и что на самом деле получается на выходе. Одно дело зазубрить «компилятор превращает исходный текст программы в машииный код», и совсем другое — иллюстрированно и по шагам проделать это самому и увидеть, что получилось.
К вашей статье можно отсылать людей, которые думают, что на современных компьютерах больше нельзя программировать в кодах :)
К вашей статье можно отсылать людей, которые думают, что на современных компьютерах больше нельзя программировать в кодах :)
Вместо заголовочного файла можно было бы просто объявить прототип функции
printf
.Да, вы правы. Об этом я не думал. Посмотрим, если наберется достаточно много интересных поправок, добавлю к статье модифицированный вариант.
«int printf(char*f,...);» всё равно выходит длиннее "#include <stdio.h>" :)
Хотя можно без int, линкер сожрёт, но с варнингом. Получится на 1 (!) символ короче.
А у меня даже варнинга не выдает. Если ставить пробел после #include, как все нормальные люди, то получится одинаковая длина.
Варнинг у меня только один. На &ptrprintf. Я хотел добавить макрос с кастом к int, чтобы оставить ту же длину и однородный цвет в таблице массива, но решил оставить как есть.
printf(char*,...);
#include <stdio.h>
Варнинг у меня только один. На &ptrprintf. Я хотел добавить макрос с кастом к int, чтобы оставить ту же длину и однородный цвет в таблице массива, но решил оставить как есть.
(ирония)
А если ставить пробел после запятой, как все нормальные люди, то всё равно получится на один символ больше.
А если ставить пробел после запятой, как все нормальные люди, то всё равно получится на один символ больше.
Тут не поспоришь :)
Но мне кажется, что вариант с прототипом вполне неплохой и поддерживает стиль остального кода.
Но мне кажется, что вариант с прототипом вполне неплохой и поддерживает стиль остального кода.
Зачем вам вообще что-то в скобках? Это же C, не C++!
«int printf();» — законное описание функции, которая принимает «что-то там, неважно что», возврщает int, большего вам и не нужно…
«int printf();» — законное описание функции, которая принимает «что-то там, неважно что», возврщает int, большего вам и не нужно…
А еще можно сразу вызывать какойнить 'system' и одним махом столько разных программ «на Си написать» :)
В Windows тоже можно использовать сисколы.
Дпугое дело, что они version-specific.
Конкретно для вывода на консоль можно использовать NtWriteFile.
Дпугое дело, что они version-specific.
Конкретно для вывода на консоль можно использовать NtWriteFile.
NtWriteFile в поток вывода? Интересно. Надо попробовать как по длине кода получится.
Плохо по длине кода получится. Потому что дескриптор потока вывода еще получить надо.
Ну есть жеж WriteConsoleA…
handle на stdout получается через GetStdHandle(-11)
Пример создаст экзешник 2,5KB размером…
для ml надо немного переделать (ну не люблю я его… и не пользуюсь)
handle на stdout получается через GetStdHandle(-11)
push -11
call GetStdHandle
под спойлером пример для nasm+link
extern _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
Пример создаст экзешник 2,5KB размером…
nasm hw.asm -f win32 -o hw.obj && link hw.obj /out:hw.exe /entry:main /defaultlib:kernel32
для ml надо немного переделать (ну не люблю я его… и не пользуюсь)
Для консоли это не будет работать, у консоли вообще нет ядерного хэндла. WriteFile, обнаружив kernel32-хэндл консоли, вызывает WriteConsole, WriteConsole работает в конечном счёте через RPC к модулю winsrv.dll в процессе csrss. (В частности, из-за такой схемы в XP консольные окна не подхватывали темы оформления — грузить абы что в системный процесс csrss MS не хотела. В Vista для этого в цепочку добавили ещё один процесс conhost.exe, вызываемый из csrss).
Ненормальное программирование!
Как-то так. Разве что 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
}
Честно говоря совсем не понял для чего столько пушей и что в итоге получается. Можете немного подробнее объяснить как это работает?
Много пушей, поскольку параметров у функции много. Правда многие равны NULL. Описание функции по ссылке.
NTWriteFile, он же ZwWriteFile
NTSTATUS 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
);
mark_ablov решил выпендрится и переписал вашу реализацию hello world включив в неё функциональность вывода на консоль базонезависимым кодом. Однако реализация эта будет работать скорее всего только на некоторых системах 8.1 x64
В принципе, туда еще небольшой шажок — поиск номера функции в теле NtWriteFile и будет веселее.
Мне кажется лучше было бы просто узнать адрес NtWriteFile через PEB_LDR_DATA и вызвать ф-ю напрямую, потому что зная номер сервиса нам ещё нужно как-то попасть в ядро, а в разных разрядностях это реализовано по-разному
Что разрядности — в разных билдах номер может отличаться. Да, и как выяснилось ниже, NtWriteFile не катит — он не работает с псевдо-handle'ами. Надо через LDR_DATA вытаскивать WriteFile или сразу WriteConsole.
del
И что, правда работает с выводом на консоль (если не перенаправлять вывод в файл)?
Отчего нет — по 1Ch лежит HANDLE StdOutput.
Хэндлы бывают разные. Вы пробовали запустить код от mark_ablov?
Win7 WOW64:
Первый запуск — перенаправление вывода, 0x6C — нормальный ядерный хэндл. Второй запуск — вывод напрямую на консоль, хэндл, как легко видеть, ненормальный (ядерные хэндлы всегда делятся на 4).
#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).
Это псевдо-хэндл. У меня на ХР выдает 7 (кстати, GetStdHandle делает именно это — роется в TEB/PEB/EPROCESS_PARAMETERS), и это рабочий handle, по крайней мере WriteFile его принимает (Nt(Zw)WriteFile не пробовал).
upd: понятно, WriteFile обрабатывает это особо, а ntdll не примет. Тогда да, вы правы.
upd: понятно, WriteFile обрабатывает это особо, а ntdll не примет. Тогда да, вы правы.
Вполне возможно, да.
Проверял из ms vc для простоты. Тогда можно RPC слать серверу консолей. Что малость сложнее, но тоже ничего невозможного.
Проверял из ms vc для простоты. Тогда можно RPC слать серверу консолей. Что малость сложнее, но тоже ничего невозможного.
Хм, интересно. Win8.1, говорите?
На Win7 ожидаемо фейлится с STATUS_OBJECT_TYPE_MISMATCH:
Восьмёрки под рукой нет. Посмотрел в 32-битной Win10, и там таки действительно консольный хэндл — настоящий ядерный хэндл на объект \Device\ConDrv, WriteFile не делает специальных проверок и просто вызывает NtWriteFile, а GetConsoleMode и прочие реализованы через NtDeviceIoControlFile вместо LPC к winsrv.dll (XP-)/conhost.exe (Vista+).
Значит, MS таки глобально переписала консоль в районе восьмёрки. Тогда да, ваш код на восьмёрке и десятке работать будет. (Априори мне версия со столь существенными изменениями от MS показалась менее вероятной, чем версия, что вы просто не проверили код. Приношу свои извинения.) Интересно, зачем… Гугль первым результатом по \Device\ConDrv выдаёт статью с описанием обхода песочницы Хрома через него.
На Win7 ожидаемо фейлится с STATUS_OBJECT_TYPE_MISMATCH:
Картинка

Восьмёрки под рукой нет. Посмотрел в 32-битной Win10, и там таки действительно консольный хэндл — настоящий ядерный хэндл на объект \Device\ConDrv, WriteFile не делает специальных проверок и просто вызывает NtWriteFile, а GetConsoleMode и прочие реализованы через NtDeviceIoControlFile вместо LPC к winsrv.dll (XP-)/conhost.exe (Vista+).
Значит, MS таки глобально переписала консоль в районе восьмёрки. Тогда да, ваш код на восьмёрке и десятке работать будет. (Априори мне версия со столь существенными изменениями от MS показалась менее вероятной, чем версия, что вы просто не проверили код. Приношу свои извинения.) Интересно, зачем… Гугль первым результатом по \Device\ConDrv выдаёт статью с описанием обхода песочницы Хрома через него.
Чрезвычайно простой и практичный подход к написанию ПО. Ждём более сложные примеры.
А еще можно попробовать переписать 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' );
}
Когда вижу подобный код в голове проскакивает непременное «За изобретение ставлю пять, а… по предмету — неуд». Это все очень хорошо и интересно, но практическое применение этого очень редко оправдано. Нисколько вас не упрекаю, если могло так показаться.
Я абсолютно с вами согласен. Это исключительный интерес. И в первую очередь проба себя в низкоуровневых областях программирования, если можно так выразиться.
Вместо хаба «Visual Studio» тогда уж «Ненормальное программирование».
Всё от задачи зависит.
Если вы пишете hello world — это одно. А если антивирус — всё уже гораздо ближе к ненормальному.
Во времена XP писал плагин для Outlook Express — так там чтобы заюзать его собственную функцию, показывающую диалог выбора папок для писем и возвращающую результат делал страшный костыль: ставил хук на создание окна; затем имитировал команду меню приложения «перейти в папку» (которая как раз приводила к вызову искомой функции), в хуке ловил появляющееся окно, дальше брал адрес локальной переменной (которая, разумеется, создаётся на стеке) и от этого адреса раскручивал вверх память, проверяя каждое слово, не является ли оно адресом из исполнимой части экзешника процесса (для этого сперва парсил заголовки PE, чтобы понять, в каком адресном диапазоне находится именно его код). Причём сделать это нужно было дважды: первый раз под эти условия попадает адрес возврата из оконной функции (тот, что лежит на стеке) а следующий — адрес возврата из искомой функции выбора диалога уже в недрах экзешника. После этого в нужном адресе отступаем назад на код команды call, и из этого машкода выцарапываем смещение нужной функции. Вычисляем адрес, проверяем, что он попадает в секцию relocation экзешника — и тогда да, это то, что нужно.
Так вот этот «костыль» успешно пошёл в продакшн; успешно взлетел на 64-битной винде; успешно взлетел позже на winmail (когда пришла vista), и насколько мне известно, пошёл «в тираж» и далее.
Так что не всякое «ненормальное программирование» даёт хрупкий и непредсказуемый результат.
Если вы пишете hello world — это одно. А если антивирус — всё уже гораздо ближе к ненормальному.
Во времена XP писал плагин для Outlook Express — так там чтобы заюзать его собственную функцию, показывающую диалог выбора папок для писем и возвращающую результат делал страшный костыль: ставил хук на создание окна; затем имитировал команду меню приложения «перейти в папку» (которая как раз приводила к вызову искомой функции), в хуке ловил появляющееся окно, дальше брал адрес локальной переменной (которая, разумеется, создаётся на стеке) и от этого адреса раскручивал вверх память, проверяя каждое слово, не является ли оно адресом из исполнимой части экзешника процесса (для этого сперва парсил заголовки PE, чтобы понять, в каком адресном диапазоне находится именно его код). Причём сделать это нужно было дважды: первый раз под эти условия попадает адрес возврата из оконной функции (тот, что лежит на стеке) а следующий — адрес возврата из искомой функции выбора диалога уже в недрах экзешника. После этого в нужном адресе отступаем назад на код команды call, и из этого машкода выцарапываем смещение нужной функции. Вычисляем адрес, проверяем, что он попадает в секцию relocation экзешника — и тогда да, это то, что нужно.
Так вот этот «костыль» успешно пошёл в продакшн; успешно взлетел на 64-битной винде; успешно взлетел позже на winmail (когда пришла vista), и насколько мне известно, пошёл «в тираж» и далее.
Так что не всякое «ненормальное программирование» даёт хрупкий и непредсказуемый результат.
За изобретение ставлю пять, а… по предмету — неуд
Тот самый троллейбус.jpg =) (Против статьи ничего не имею, если что)
На самом деле неплохой прием для обфускации кода.
Ну разве что исходного. Дизассеблер будет выглядеть как код составленный заботливым разработчиком (:
Против дизассемблирования есть другие приемы) Данный метод обфускации хорош при работе с недобросовестным заказчиком, если вдруг по договору исходный код должен передаваться. Обычно такие дяди имеют на борту студента, который может собрать софтину и проанализировать код на закладки типа «Демонстрационная Версия». Конечно подобные договора не стоит заключать, но иногда выхода нет (начальство недальновидное, принципиальная необходимость выполнить эти работы, госзаказ и т.д. и т.п.).
Конечно подобные договора не стоит заключать, но иногда выхода нетЛибо я ничего не понимаю, либо одно из двух.
Не знаю как там насчёт «нодобросовестного заказчика», но передача подобных кодов — это уж точно «недобросовестный исполнитель».
Критерием «недобросовестного исполнителя» является передача частично обфусцированного кода или факт самой передачи кода?
Сама передача кода мне кажется вообще «дефолтным» поведением (зачем мне компонент без исходников?), хотя, конечно, она должна оговариваться в договоре. А вот передача обфусцированного кода — это уже повод для того, чтобы разорвать отношения и больше никогда к подобному поставщику не обращаться.
Причём независимо от того, будут ли в принципе выданы коды лучше компонент, полученный от такого поставщика переписать при первой возможности: всех оставленныъ лазеек простым просмотром не выявить, а если разбираться досконально и вычитывать каждую строчку — так можно и с нуля всё сделать примерно за те же деньги.
Другое дело если вам передают компонент не специально для вас разработанный, а такой, в который соответствующая компания вложила годы разработки. Тут нужно явно оговорить, что, наоборот, передаются только бинарники, исходники не передаются. А переписывать ли такой компонент — нужно судить по бизнес-обстановке. Если он вам нужен «всерьёз и надолго» (то есть на нём ваш бизнес «висеть» будет) — лучше переписать.
P.S. Я, разумеется, говорю про IT компании и случай когда вас же поставщик может решить обойтись без вас в качестве посредника. Конечно переписывать системы складского учёта я не предлагаю (представить себе что фирма по внедрению 1С начнёт вдруг «грузить апельсины бочками» достаточно сложно).
Причём независимо от того, будут ли в принципе выданы коды лучше компонент, полученный от такого поставщика переписать при первой возможности: всех оставленныъ лазеек простым просмотром не выявить, а если разбираться досконально и вычитывать каждую строчку — так можно и с нуля всё сделать примерно за те же деньги.
Другое дело если вам передают компонент не специально для вас разработанный, а такой, в который соответствующая компания вложила годы разработки. Тут нужно явно оговорить, что, наоборот, передаются только бинарники, исходники не передаются. А переписывать ли такой компонент — нужно судить по бизнес-обстановке. Если он вам нужен «всерьёз и надолго» (то есть на нём ваш бизнес «висеть» будет) — лучше переписать.
P.S. Я, разумеется, говорю про IT компании и случай когда вас же поставщик может решить обойтись без вас в качестве посредника. Конечно переписывать системы складского учёта я не предлагаю (представить себе что фирма по внедрению 1С начнёт вдруг «грузить апельсины бочками» достаточно сложно).
Когда ваш исходный код окажется в третьей конторе, и она волшебным образом получит этот проект на исполнение после разрыва договора с вами… Я всего лишь предположил, что в случаях крайней необходимости можно этим пользоваться. Ведь проведение подобной обфускации крайне затратное дело. Лично я считаю, что с мутными товарищами, что заказчиками, что подрядчиками, лучше дел не иметь. Но директорату, как говорится, виднее. Бывает ещё так, что заказчик навязывает вам субподрядчика, который ничего не делает, но, внезапно, должен иметь доступ к исходному коду.
Практические знания подобного рода становятся очень сильно востребованными, когда, например, поднимаешь тулчейн для новой embedded железки: писать так не нужно, но понимать, что происходит под капотом — это просто необходимо. Особенно когда за байты война начинается.
На мой взгляд, экзешник большой выходит. С /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)
#pragma comment(linker, "/FILEALIGN:16")
#pragma comment(linker, "/ALIGN:16")// Merge sections
… и получить файл, не работающий на x64.
Ну так это ж «ненормальное программирование» :) Я ж не говорю о том, что исходник и так на других ОС, компиляторах и архитектурах не работает
Интересно почему не рабочий?
А это у MS спрашивать надо. Если 32-битный бинарник собран с выравниванием меньше размера страницы (и некоторыми дополнительными ограничениями), то 32-битная система его загрузит, а 64-битная — нет.
Просто насколько я знаю минимально допустимое выравнивание для PE это file:0x200, virtual:0x1000, у вас есть пруф бинарника с более низким выравниванием?
Возьмите любой драйвер.
Если настаиваете именно на exe-шнике — вот банальный MessageBox с FileAlignment = SectionAlignment = 0x20: yadi.sk/d/385e5Lhqnkybi. На 32-битной XP точно работает.
Если настаиваете именно на exe-шнике — вот банальный MessageBox с FileAlignment = SectionAlignment = 0x20: yadi.sk/d/385e5Lhqnkybi. На 32-битной XP точно работает.
Конечно, 16/16 отлично работает на XP, только вчера проверял.
Стесняюсь спросить, а всякие прерывания INT 21h и более системные (15 что-ли) для вывода символов на терминал уже не работают? Раньше можно покороче сделать было байт-код.
Раньше можно покороче сделать было байт-код.
Где это работало?
На сколько я знаю в windows прерывания заблокированы начиная с windows 2000.
Если вы соберете под MS-DOS чистый MZ exeшник, то будет работать, и то не во всех системах.
Я тне пргограммист, поэтому спрошу прямо — во что дизассемблер разберет сгенерированный из этой рпограммы машинный код?
Собственно это указано ниже «Компилируем и смотрим дизассемблер.»
Ну, плюс секция импорта и т.п.
Ну, плюс секция импорта и т.п.
Ой, а я почему-то думал, что дизассемблер в фугкции собирает, типа программу на си выдает, которая компилируется в такой же ассемблерный код
Я ассемблер не знаю, собственно вопрос в том — какой «безхитростный» код даст аналогичный ассемблерный листинг? С нормальным мейном-функцией, принтэфом и строкой в параметре или что-то другое?
Т.е. вопрос собственно в том, с чем автор в итоге поигрался — только с кодом или с кодом и ассемблером или с конечной программой тоже?
Я ассемблер не знаю, собственно вопрос в том — какой «безхитростный» код даст аналогичный ассемблерный листинг? С нормальным мейном-функцией, принтэфом и строкой в параметре или что-то другое?
Т.е. вопрос собственно в том, с чем автор в итоге поигрался — только с кодом или с кодом и ассемблером или с конечной программой тоже?
Ой, а я почему-то думал, что дизассемблер в фугкции собирает, типа программу на си выдает, которая компилируется в такой же ассемблерный код
Это называется декомпилятор.
с чем автор в итоге поигрался
Да ни с чем. Написал на ассемблере вызов 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
Классная опечатка, я теперь тоже буду «the программистом»
В этом комментарии я показал во что превращается main. В то же самое, что и вставили в массив, компилятор никак не изменил этот код, потому что для него это были данные, а не код.
Как собрать рабочий экзешник на Visual Studio вообще без исходника:
http://rsdn.ru/forum/humour/3748806.all
link /OUT:dummy.exe /SUBSYSTEM:CONSOLE /ENTRY:ExitProcess@4 kernel32.lib
http://rsdn.ru/forum/humour/3748806.all
Автор, а куда в исполняемом файле компилятор в итоге втыкает EntryPoint — в Вашу секцию .exre? Или он в итоге в .code подставляет переход (jmp) к .exre?
Точно так же, как если бы main был функцией. main является EntryPoint по умолчанию.
Вызывается
Из него вызывается
Затем сам
А main лежит в секции
И как видно по этому адресу лежит непосредственно наш
Вызывается
mainCRTStartup

Из него вызывается
__tmainCRTStartup

Затем сам
main

А main лежит в секции
.exre

И как видно по этому адресу лежит непосредственно наш
main

Это можно указать прямо:
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.exe
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.exe
Прошу прощения, перепутал секцию .code и .text)
В общем понятно, компилятор-таки городит городули с JMP к новой секции.
Просто технически ничего не мешает поменять адрес EntryPoint сразу на .exre, чтобы избежать лишних переходов.
А сможете теперь заставить компилятор переходить сразу к выполнению .exre? Беру Вас на «слабо»! =)
И у Вас там, похоже, еще и рантайм вкомпиливается, судя по вызовам __crtUnhandledException?
В общем понятно, компилятор-таки городит городули с JMP к новой секции.
Просто технически ничего не мешает поменять адрес EntryPoint сразу на .exre, чтобы избежать лишних переходов.
А сможете теперь заставить компилятор переходить сразу к выполнению .exre? Беру Вас на «слабо»! =)
И у Вас там, похоже, еще и рантайм вкомпиливается, судя по вызовам __crtUnhandledException?
Я думал об этом, ещё до того как написал этот «Hello, World!», но так и не осилили. На данный момент я плохо знаю структуру exe файла и тонкости компиляторов.
И да, в том экзешнике куча рантайма и дебага.
И да, в том экзешнике куча рантайма и дебага.
Почитайте про структуру не exe файла, а формата PE в целом (Portable Executable). Что DLL, что EXE суть один и тот же формат.
Ничего сложного нет, и кстати формат ELF в Linux схематично очень похож на PE, так что в любом случае будет полезно)
Вот вроде бы неплохо структурированная информация: cs.usu.edu.ru/docs/pe
Раньше это все было на wasm.ru, но сейчас у них так какие-то обновления происходят.
Ничего сложного нет, и кстати формат ELF в Linux схематично очень похож на PE, так что в любом случае будет полезно)
Вот вроде бы неплохо структурированная информация: cs.usu.edu.ru/docs/pe
Раньше это все было на wasm.ru, но сейчас у них так какие-то обновления происходят.
Так а ничего мудрёного же нет.
В свойствах проекта, в дереве «Configuration Properties -> Linker -> Advanced», первый пункт «Entry Point» установить в «main».
Всё.
Теперь из проекта компилируется экзешник размером 2560 байт — без рантайма, без __tmainCRTStartup, без __crtUnhandledException, и даже без секции .text.
Ещё можно отключить внедрение манифеста, тогда экзешник будет 1536 байт.
И всё это — стандартными средствами Студии, безо всякого шаманства с заголовками PE :-)
В свойствах проекта, в дереве «Configuration Properties -> Linker -> Advanced», первый пункт «Entry Point» установить в «main».
Всё.
Теперь из проекта компилируется экзешник размером 2560 байт — без рантайма, без __tmainCRTStartup, без __crtUnhandledException, и даже без секции .text.
Ещё можно отключить внедрение манифеста, тогда экзешник будет 1536 байт.
И всё это — стандартными средствами Студии, безо всякого шаманства с заголовками PE :-)
И правда ведь. Неужели я так криво пробовал настройки, что решил, что это не так работает. Спасибо, много полезного на хабре узнать можно.
И еще, не подскажете ли такую вещь: в «Entry Point» можно любую точку входа вставить или только определенные? Где-то прочел, что линковщик ожидает определенные.
И еще, не подскажете ли такую вещь: в «Entry Point» можно любую точку входа вставить или только определенные? Где-то прочел, что линковщик ожидает определенные.
Ну-ка, братцы, а вот так работает?
Без printf и без сисколов! Только массив и больше ничего!
Ищет в памяти kernel32, и потом у него в таблице экспорта ищет WriteFile:
Без printf и без сисколов! Только массив и больше ничего!
Ищет в памяти kernel32, и потом у него в таблице экспорта ищет WriteFile:
#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};
Проверял на Windows 7.По ординалу?
Нет, по имени.
Просто лень в дизасм загонять — код короткий, вряд ли полноценно по имени ищет, или там упрощенное сравнение?
Там сравнение хэша от имени с магической константой.
Да, так и есть.
Причём код для побайтного сравнения имени функции был бы ещё короче: благо,
Причём код для побайтного сравнения имени функции был бы ещё короче: благо,
repe cmpsb
занимает всего два байта.Как раз о хэше и думал, стандартный ход.
Я на первопроходство и не претендовал; но длина кода тут совершенно ни при чём. Вот код с побайтовым сравнением, он даже на три байта короче вышел.
#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 };
Просто хэш-функции разные бывают. Это может быть и CRC32, что солидно больше места занимает.
Предлагаю вам взять в руки Студию и реализовать тот же самый код ещё короче, с использованием любой хэш-функции по вашему вкусу :-)
Зачем? Мне безразлична длина вашего кода. Я просто высказал предположение, на глаз, что судя по длине, используется простая хэш-функция. А вы уже соревнование затеваете, будто я претензии к длине кода предъявляю :) Я вчера уже ужимал HW exeшник с обычными импортами до минимума, так что свое спортивное любопытство уже удовлетворил (меньше 1к). К тому же на Си я почти не пишу.
И, если честно, мне просто лень переводить ваш массив обратно в код, иначе бы я не спросил про способ поиска.
И, если честно, мне просто лень переводить ваш массив обратно в код, иначе бы я не спросил про способ поиска.
Я просто высказал предположение, на глаз, что судя по длине, используется простая хэш-функция.
Вы совершенны правы в том, что используется простая хэш-функция.
Но я на протяжении всей ветки комментариев пытаюсь объяснить, что такой вывод не стоило делать на основании длины кода, потому что и без использования хэш-функции код длиннее не становится.
Нет-нет, мой ход мыслей был таков «для сложного хеша кода маловато, наверно что-то простое. Ну вряд ли же это просто сравнение». Т.е. обычное сравнение почти не рассматривалось. Я просто плохо подобрал слова в вопросе «код короткий, вряд ли полноценно по имени ищет, или там упрощенное сравнение?» здесь и имелось в виду, что вряд ли сравнение — это неспортивно, наверно короткая х.ф.
Кстати, сколько ваш итоговый бинарник весит?
Кстати, сколько ваш итоговый бинарник весит?
1536 байт после отключения рантайма и манифеста.
Если ещё и отладочную информацию отключить, остаётся ровно килобайт.
Дальше, думаю, встроенными средствами Студии не ужать, придётся ковыряться в бинарнике руками.
Если ещё и отладочную информацию отключить, остаётся ровно килобайт.
Дальше, думаю, встроенными средствами Студии не ужать, придётся ковыряться в бинарнике руками.
Ну собственно, оказалось достаточно стереть все нули из конца файла и поменять SizeOfRawData в заголовке секции, чтобы остался работоспособный экзешник в 680 байт.
(У меня система x64, на ней /FILEALIGN:16 не запускается.)
А у вас сколько байт было в итоге?
(У меня система x64, на ней /FILEALIGN:16 не запускается.)
А у вас сколько байт было в итоге?
816 после линкреа без копания руками, но с выравниванием по 16. Можно ужать еще за счет заголовков.
Ага, 716 с одной секцией. Еще 256 dosstub а надо извлечь.
Ага, 716 с одной секцией. Еще 256 dosstub а надо извлечь.
Чтобы запускалось на x86, /FILEALIGN должен быть как минимум 512, т.е. все заголовки вместе взятые меньше чем 512 байт занимать не могут.
Активным «копанием руками в бинарнике» мне удалось уменьшить свой файл до 653 байт:
Дальше пока не понимаю как уменьшать.
Активным «копанием руками в бинарнике» мне удалось уменьшить свой файл до 653 байт:
base64
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAADdHerZmXyEipl8hIqZfISKhy4Niph8hIqHLhWKmHyEilJpY2iZfISKAAAAAAAAAABQRQAATAEBAPLwqFYAAAAAAAAAAOAAAgELAQkAAAAAAAACAAAAAAAAABAAAAAQAAAAEAAAAABAAAAQAAAAAgAABQAAAAAAAAAFAAAAAAAAAAAgAAAAAgAAAAAAAAMAQIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5leHJlAAAAkAAAAAAQAACFAAAAAAIAAAAAAAAAAAAAAAAAAEAAAGBIZWxsbywgV29ybGQhDQoAV3JpdGVGaWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqAFRqD+gAAAAAgSwkOg4AADPSZItSMItCEP9wHP90JASDBCQQ/ItSDItSFItyKLkYAAAAM/8zwKw8YXwCLCDBzw0D+OLyi1oQgf9bvEpqixJ12YtTPItsE3gD64tVGItFIAPDSos0kAPzuQoAAACLPCTzpnXui0UkA8NmiwxQi0UcA8OLBJADw1v/0MM=
Дальше пока не понимаю как уменьшать.
Чтобы запускалось на x86, /FILEALIGN должен быть как минимум 512, т.е. все заголовки вместе взятые меньше чем 512 байт занимать не могут.
Минимальный FILEALIGN — 16. Я довел до 640 и бросил, после работы покопаюсь еще.
Да, извиняюсь, я имел в виду «Чтобы запускалось на x64, /FILEALIGN должен быть как минимум 512». Комментаторы выше это отмечали тоже.
Я тем временем додавил свой экзешник до 365 байт, и он у меня по-прежнему запускается на x64 :-)
Покажу потом, чтобы спортивный интерес не отбивать.
Я тем временем додавил свой экзешник до 365 байт, и он у меня по-прежнему запускается на x64 :-)
Покажу потом, чтобы спортивный интерес не отбивать.
365 с импортами? Куда далее идти и так понятно — надо схлопывать MZ, PE и Optional Header.
Без «настоящих» импортов — точно так же ищет в памяти нужные модуль и функцию, как раньше.
А, ну я-то модуль с секцией импорта ужимаю. «Так» можно, конечно, еще меньше. Мне сильно мешает вкомпилированный код для SEH цепочки. Если дальше жать, это будет уже малость нечестно, потому что я покромсаю то, что создал компилятор.
Сейчас у меня 640, если выкинуть «лишний» код для SEH, можно примерно 64 байта выиграть. Потом заголовки, но проблема в том, что для импорта нужен optional header.
Сейчас у меня 640, если выкинуть «лишний» код для SEH, можно примерно 64 байта выиграть. Потом заголовки, но проблема в том, что для импорта нужен optional header.
Optional header, несмотря на название, обязателен в любом случае — например, точка входа записывается именно там.
Но я в своём файле нисколько не стеснялся кромсать всё то, что создал компилятор :-)
Но я в своём файле нисколько не стеснялся кромсать всё то, что создал компилятор :-)
Теоретически минимальный размер файла, способного запускаться на 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!».Да, я уже нашёл этот пример.
Я правильно понимаю, что он не запустится на системах с принудительным DEP?
Я правильно понимаю, что он не запустится на системах с принудительным DEP?
Теоретически — должен. Если SectionAlignment < PageSize, то ядро маппит вообще всё, начиная с заголовка, как PAGE_EXECUTE_READWRITE. 32-битный загрузчик из WOW64-подсистемы потом пытается исправить атрибуты секций (из-за чего файлы, просто собранные с /align:16, и падают), но не трогает заголовок.
Практически — не проверял.
Практически — не проверял.
Для 32 еще меньше, 133 байта.
Это для совсем старых систем, до какого-то сервиспака XP. На XP SP3 и позже уже минимум 252 байта (
4 + sizeof(IMAGE_NT_HEADERS32)
).Нет, холостой 133 загружается без проблем. По крайней мере «not a valid win32 application» нет.
Как MZ-stub, ага. В Process Monitor посмотрите — ntvdm.exe там запускается.
Нет, PE — импорт kernel32 есть
ntvdm.exe тоже загружает kernel32.
Нет, нет. PE файл, прогнал через WinDbg.
Это все модули, во время BP около точки входа.
start end module name
00400000 00400104 image00400000 C (no symbols)
7c800000 7c8f6000 kernel32 (deferred)
7c900000 7c9b2000 ntdll (pdb symbols)
Это все модули, во время BP около точки входа.
> 00400000 00400104
Выглядит как 260 байт.
Выглядит как 260 байт.
А файл — внезапно — 133.
На XP SP3?
У меня на руках виртуалка с ntoskrnl.exe версии 5.1.2600.5512, в ней подобное не проходит.
У меня на руках виртуалка с ntoskrnl.exe версии 5.1.2600.5512, в ней подобное не проходит.
Забавно. Если файл есть в кэше данных — может быть короче 252 байт. Если нет (тот же самый файл на сетевом диске без дополнительных условий, или тот же самый файл на свежевставленной флешке либо после перезагрузки системы при прямой загрузке в WinDbg) — будет ntvdm.
В семёрке шизофрению пофиксили, там не грузится консистентно.
В семёрке шизофрению пофиксили, там не грузится консистентно.
На XP, кстати экзешник не запускается, если ничего не импортируешь из kernel32.dll (прямо или через другие dll).
Круто! Работает на win10. А вы сразу в ассемблере писали или на си код есть?
А во времена DOS труЪ-программеры делали так:
copy con program.exe
copy con program.exe
Sign up to leave a comment.
«Hello World!» на C массивом int main[]