Я хотел бы рассказать о том, как я писал реализацию «Hello, World!» на C. Для подогрева сразу покажу код. Кого интересует как до этого доходил я, добро пожаловать под кат.
#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 и можно объявить переменную так, чтобы она оказалась в секции с разрешением на исполнение.
Проверив, так ли это, я убедился, что это работает и функция массив main спокойно исполняет opcode ret и не вызывает ошибки «Access violation».
#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!"
push 0x726F5720 ; " Wor"
push 0x2C6F6C6C ; "llo,"
push 0x65480000 ; "He"
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 и использовать другой код вызова функции, но я не успел исследовать этот вопрос.
Буду рад вашим отзывам и замечаниям.
Автор: ComradeAndrew