Pull to refresh

Comments 143

Статья действительно «для самых маленьких» и это — отлично. Прекрасный способ «порвать шаблон» людям, которые, немного попрограммировав на каком-то языке (например, Си), не понимают, как работает компилятор и что на самом деле получается на выходе. Одно дело зазубрить «компилятор превращает исходный текст программы в машииный код», и совсем другое — иллюстрированно и по шагам проделать это самому и увидеть, что получилось.

К вашей статье можно отсылать людей, которые думают, что на современных компьютерах больше нельзя программировать в кодах :)
Вместо заголовочного файла можно было бы просто объявить прототип функции printf.
Да, вы правы. Об этом я не думал. Посмотрим, если наберется достаточно много интересных поправок, добавлю к статье модифицированный вариант.
«int printf(char*f,...);» всё равно выходит длиннее "#include <stdio.h>" :)
Хотя можно без int, линкер сожрёт, но с варнингом. Получится на 1 (!) символ короче.
А у меня даже варнинга не выдает. Если ставить пробел после #include, как все нормальные люди, то получится одинаковая длина.
printf(char*,...);
#include <stdio.h>

Варнинг у меня только один. На &ptrprintf. Я хотел добавить макрос с кастом к int, чтобы оставить ту же длину и однородный цвет в таблице массива, но решил оставить как есть.
(ирония)
А если ставить пробел после запятой, как все нормальные люди, то всё равно получится на один символ больше.
Тут не поспоришь :)
Но мне кажется, что вариант с прототипом вполне неплохой и поддерживает стиль остального кода.
Зачем вам вообще что-то в скобках? Это же C, не C++!

«int printf();» — законное описание функции, которая принимает «что-то там, неважно что», возврщает int, большего вам и не нужно…
Да, действительно. На тулсете 2013 студии компилирует. Однако при компиляции тулсетом v140(visual studio 2015) она кидает error:
error LNK2001: unresolved external symbol _printf
Скорее всего как-то манглится имя. Посмотрите в обычном «Hello, world!» чего хочет ваш объектник…
А еще можно сразу вызывать какойнить 'system' и одним махом столько разных программ «на Си написать» :)
В Windows тоже можно использовать сисколы.
Дпугое дело, что они version-specific.
Конкретно для вывода на консоль можно использовать NtWriteFile.
NtWriteFile в поток вывода? Интересно. Надо попробовать как по длине кода получится.
Плохо по длине кода получится. Потому что дескриптор потока вывода еще получить надо.
Ну есть жеж WriteConsoleA…
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 надо немного переделать (ну не люблю я его… и не пользуюсь)
Можно меньше, если выравнивание секций поменять, если я не путаю.
Если есть mingw (или cygwin), то "strip --strip-all hw.exe" оставит от экзешника 2KB
Туда еще wipedosstub, наверно можно ужать сильнее. + минимальное выравние секции в файле 64, емнип.
Что-то много. 1 КБ, не трогая выравнивание, на FASM-e.
Для консоли это не будет работать, у консоли вообще нет ядерного хэндла. 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.
Дело не в номерах, а в особенностях механизмов входа в ядро для x86 и x64(WOW64). Во-первых они отличаются, во-вторых если мне не изменяет память для x64 необходимо класть параметры с учётом того что они будут обрабатываться 64-битным кодом.

Так что да, лучше юзать что-то высокоуровневое

Мы так и так компилируем 32 разрядный код в примере выше + стандартная точка входа из TEB. А вот номера сервисов отличаются от SP к SP, например.
call fs:[0C0h] в Windows Vista и выше не будет работать для x86, там используется SharedUserData->SystemCallStub
И что, правда работает с выводом на консоль (если не перенаправлять вывод в файл)?
Отчего нет — по 1Ch лежит HANDLE StdOutput.
Хэндлы бывают разные. Вы пробовали запустить код от 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).
Это псевдо-хэндл. У меня на ХР выдает 7 (кстати, GetStdHandle делает именно это — роется в TEB/PEB/EPROCESS_PARAMETERS), и это рабочий handle, по крайней мере WriteFile его принимает (Nt(Zw)WriteFile не пробовал).

upd: понятно, WriteFile обрабатывает это особо, а ntdll не примет. Тогда да, вы правы.
Рабочий для NtWriteFile? Проверяли? Что он рабочий для WriteFile, сомнений нет.
Да, я уже посмотрел листинг — kernel32 перенаправляет это в WriteConsoleA.
Вполне возможно, да.
Проверял из 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 выдаёт статью с описанием обхода песочницы Хрома через него.
> WriteFile не делает специальных проверок и просто вызывает NtWriteFile
Угу, я когда смотрел как она преобразовывает STD_OUTPUT_HANDLER не видел никаких вызовов WriteConsole, а просто обращение к данным PEBа.
Чрезвычайно простой и практичный подход к написанию ПО. Ждём более сложные примеры.
Если правда интересно, то постараюсь придумать что-то похожее. Пока времени мало, но как сдам сессию наверняка вернусь к этому. Я даже не ожидал, что будет такой интерес :)
А еще можно попробовать переписать 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), и насколько мне известно, пошёл «в тираж» и далее.

Так что не всякое «ненормальное программирование» даёт хрупкий и непредсказуемый результат.
За изобретение ставлю пять, а… по предмету — неуд

Тот самый троллейбус.jpg =) (Против статьи ничего не имею, если что)
На самом деле неплохой прием для обфускации кода.
Ну разве что исходного. Дизассеблер будет выглядеть как код составленный заботливым разработчиком (:
Против дизассемблирования есть другие приемы) Данный метод обфускации хорош при работе с недобросовестным заказчиком, если вдруг по договору исходный код должен передаваться. Обычно такие дяди имеют на борту студента, который может собрать софтину и проанализировать код на закладки типа «Демонстрационная Версия». Конечно подобные договора не стоит заключать, но иногда выхода нет (начальство недальновидное, принципиальная необходимость выполнить эти работы, госзаказ и т.д. и т.п.).
Конечно подобные договора не стоит заключать, но иногда выхода нет
Либо я ничего не понимаю, либо одно из двух.

Не знаю как там насчёт «нодобросовестного заказчика», но передача подобных кодов — это уж точно «недобросовестный исполнитель».
Критерием «недобросовестного исполнителя» является передача частично обфусцированного кода или факт самой передачи кода?
Сама передача кода мне кажется вообще «дефолтным» поведением (зачем мне компонент без исходников?), хотя, конечно, она должна оговариваться в договоре. А вот передача обфусцированного кода — это уже повод для того, чтобы разорвать отношения и больше никогда к подобному поставщику не обращаться.

Причём независимо от того, будут ли в принципе выданы коды лучше компонент, полученный от такого поставщика переписать при первой возможности: всех оставленныъ лазеек простым просмотром не выявить, а если разбираться досконально и вычитывать каждую строчку — так можно и с нуля всё сделать примерно за те же деньги.

Другое дело если вам передают компонент не специально для вас разработанный, а такой, в который соответствующая компания вложила годы разработки. Тут нужно явно оговорить, что, наоборот, передаются только бинарники, исходники не передаются. А переписывать ли такой компонент — нужно судить по бизнес-обстановке. Если он вам нужен «всерьёз и надолго» (то есть на нём ваш бизнес «висеть» будет) — лучше переписать.

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 точно работает.
Действительно робит (проверено на w7x86), спасибо
Конечно, 16/16 отлично работает на XP, только вчера проверял.
Стесняюсь спросить, а всякие прерывания INT 21h и более системные (15 что-ли) для вывода символов на терминал уже не работают? Раньше можно покороче сделать было байт-код.
Раньше можно покороче сделать было байт-код.

Где это работало?
На сколько я знаю в windows прерывания заблокированы начиная с windows 2000.
Не совсем. Вплоть до XP SP2(?) syscall идет через int 2Eh…
но мы понимаем, что вопрос выше чуть о другом ;)
Если вы соберете под 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. В то же самое, что и вставили в массив, компилятор никак не изменил этот код, потому что для него это были данные, а не код.
Автор, а куда в исполняемом файле компилятор в итоге втыкает EntryPoint — в Вашу секцию .exre? Или он в итоге в .code подставляет переход (jmp) к .exre?
Точно так же, как если бы main был функцией. main является EntryPoint по умолчанию.
Вызывается
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
/SECTION:.text,ERW
Это что, устанавливает в секции все атрибуты? Ого.
Угу, потом сливаются все секции в одну, изгоняется на мороз DOS stub и получается маленький файл.
Прошу прощения, перепутал секцию .code и .text)

В общем понятно, компилятор-таки городит городули с 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, но сейчас у них так какие-то обновления происходят.
Спасибо, вы кстати. Не так давно искал где бы понятнее и подробнее можно об этом почитать.
Так а ничего мудрёного же нет.

В свойствах проекта, в дереве «Configuration Properties -> Linker -> Advanced», первый пункт «Entry Point» установить в «main».
Всё.
Теперь из проекта компилируется экзешник размером 2560 байт — без рантайма, без __tmainCRTStartup, без __crtUnhandledException, и даже без секции .text.

Ещё можно отключить внедрение манифеста, тогда экзешник будет 1536 байт.
И всё это — стандартными средствами Студии, безо всякого шаманства с заголовками PE :-)
И правда ведь. Неужели я так криво пробовал настройки, что решил, что это не так работает. Спасибо, много полезного на хабре узнать можно.
И еще, не подскажете ли такую вещь: в «Entry Point» можно любую точку входа вставить или только определенные? Где-то прочел, что линковщик ожидает определенные.
Любую. Определенные — это значения по умолчанию.
Ну-ка, братцы, а вот так работает?
Без 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к). К тому же на Си я почти не пишу.
И, если честно, мне просто лень переводить ваш массив обратно в код, иначе бы я не спросил про способ поиска.
Я просто высказал предположение, на глаз, что судя по длине, используется простая хэш-функция.

Вы совершенны правы в том, что используется простая хэш-функция.
Но я на протяжении всей ветки комментариев пытаюсь объяснить, что такой вывод не стоило делать на основании длины кода, потому что и без использования хэш-функции код длиннее не становится.
Нет-нет, мой ход мыслей был таков «для сложного хеша кода маловато, наверно что-то простое. Ну вряд ли же это просто сравнение». Т.е. обычное сравнение почти не рассматривалось. Я просто плохо подобрал слова в вопросе «код короткий, вряд ли полноценно по имени ищет, или там упрощенное сравнение?» здесь и имелось в виду, что вряд ли сравнение — это неспортивно, наверно короткая х.ф.

Кстати, сколько ваш итоговый бинарник весит?
Ну собственно, оказалось достаточно стереть все нули из конца файла и поменять SizeOfRawData в заголовке секции, чтобы остался работоспособный экзешник в 680 байт.

(У меня система x64, на ней /FILEALIGN:16 не запускается.)

А у вас сколько байт было в итоге?
816 после линкреа без копания руками, но с выравниванием по 16. Можно ужать еще за счет заголовков.
Ага, 716 с одной секцией. Еще 256 dosstub а надо извлечь.
Чтобы запускалось на x86, /FILEALIGN должен быть как минимум 512, т.е. все заголовки вместе взятые меньше чем 512 байт занимать не могут.

Активным «копанием руками в бинарнике» мне удалось уменьшить свой файл до 653 байт:
base64
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAADdHerZmXyEipl8hIqZfISKhy4Niph8hIqHLhWKmHyEilJpY2iZfISKAAAAAAAAAABQRQAATAEBAPLwqFYAAAAAAAAAAOAAAgELAQkAAAAAAAACAAAAAAAAABAAAAAQAAAAEAAAAABAAAAQAAAAAgAABQAAAAAAAAAFAAAAAAAAAAAgAAAAAgAAAAAAAAMAQIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5leHJlAAAAkAAAAAAQAACFAAAAAAIAAAAAAAAAAAAAAAAAAEAAAGBIZWxsbywgV29ybGQhDQoAV3JpdGVGaWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqAFRqD+gAAAAAgSwkOg4AADPSZItSMItCEP9wHP90JASDBCQQ/ItSDItSFItyKLkYAAAAM/8zwKw8YXwCLCDBzw0D+OLyi1oQgf9bvEpqixJ12YtTPItsE3gD64tVGItFIAPDSos0kAPzuQoAAACLPCTzpnXui0UkA8NmiwxQi0UcA8OLBJADw1v/0MM=

Дальше пока не понимаю как уменьшать.
Чтобы запускалось на x86, /FILEALIGN должен быть как минимум 512, т.е. все заголовки вместе взятые меньше чем 512 байт занимать не могут.

Минимальный FILEALIGN — 16. Я довел до 640 и бросил, после работы покопаюсь еще.
Да, извиняюсь, я имел в виду «Чтобы запускалось на x64, /FILEALIGN должен быть как минимум 512». Комментаторы выше это отмечали тоже.

Я тем временем додавил свой экзешник до 365 байт, и он у меня по-прежнему запускается на x64 :-)

Покажу потом, чтобы спортивный интерес не отбивать.
365 с импортами? Куда далее идти и так понятно — надо схлопывать MZ, PE и Optional Header.
Без «настоящих» импортов — точно так же ищет в памяти нужные модуль и функцию, как раньше.
А, ну я-то модуль с секцией импорта ужимаю. «Так» можно, конечно, еще меньше. Мне сильно мешает вкомпилированный код для SEH цепочки. Если дальше жать, это будет уже малость нечестно, потому что я покромсаю то, что создал компилятор.
Сейчас у меня 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?
Теоретически — должен. Если SectionAlignment < PageSize, то ядро маппит вообще всё, начиная с заголовка, как PAGE_EXECUTE_READWRITE. 32-битный загрузчик из WOW64-подсистемы потом пытается исправить атрибуты секций (из-за чего файлы, просто собранные с /align:16, и падают), но не трогает заголовок.
Практически — не проверял.
Проверил. Работает. Поразительно.

Но с принудительным ASLR всё-таки валится.

— Ага-а-а! — с облегчением сказали суровые сибирские лесорубы, и ушли валить лес топорами.
Ну включите флаг IMAGE_FILE_RELOCS_STRIPPED = 1 в IMAGE_FILE_HEADER.Characteristics (смещение +16h от начала PE-заголовка, 2 -> 3). Будет работать и с принудительным ASLR.
ХитрО. Действительно, если релоков нет, не поперемещаешь.
Это для совсем старых систем, до какого-то сервиспака XP. На XP SP3 и позже уже минимум 252 байта (4 + sizeof(IMAGE_NT_HEADERS32)).
Нет, холостой 133 загружается без проблем. По крайней мере «not a valid win32 application» нет.
Как MZ-stub, ага. В Process Monitor посмотрите — ntvdm.exe там запускается.
Нет, нет. PE файл, прогнал через WinDbg.
start    end        module name
00400000 00400104   image00400000 C (no symbols)           
7c800000 7c8f6000   kernel32   (deferred)             
7c900000 7c9b2000   ntdll      (pdb symbols) 

Это все модули, во время BP около точки входа.
На XP SP3?
У меня на руках виртуалка с ntoskrnl.exe версии 5.1.2600.5512, в ней подобное не проходит.
Ага, на нем, родимом, сижу. Файл вообще веселый — file&section align — 4, размер OptionalHeader — 4, MZ тоже по сути только 4 байта. И оно работает.

Хех, & sect преобразовалось в параграф.

5.1.2600.6419
Забавно. Если файл есть в кэше данных — может быть короче 252 байт. Если нет (тот же самый файл на сетевом диске без дополнительных условий, или тот же самый файл на свежевставленной флешке либо после перезагрузки системы при прямой загрузке в WinDbg) — будет ntvdm.
В семёрке шизофрению пофиксили, там не грузится консистентно.
Сейчас попробую перезагрузить и сразу открыть в WinDBG, посмотрим.
И вправду, забавно. Но так или иначе, 133 загрузить можно B), пусть только в SP3.
На XP, кстати экзешник не запускается, если ничего не импортируешь из kernel32.dll (прямо или через другие dll).
Круто! Работает на win10. А вы сразу в ассемблере писали или на си код есть?
Сразу в ассемблере; но большую часть кода не писал сам, а утянул из гуляющих по интернету листингов, которые легко гуглятся по использованным в них «магическим значениям».
А во времена DOS труЪ-программеры делали так:
copy con program.exe
Sign up to leave a comment.

Articles