«Hello World!» на C масивом int main[]

Я хотів би розповісти про те, як я писав реалізацію «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!\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 

Звідси нам потрібно взяти байти коду.

Щоб вручну не прибирати асемблерний код можна скористатися регулярними виразами в notepad++.Регулярний вираз для послідовності після байтів коду:

{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 у нас йшов наступний елемент, куди ми і будемо поміщати нашу адресу.

Шляхом нехитрих підстановки отримуємо шукану послідовність.Беремо отриману раніше послідовність байт нашого асемблерного коду. Відштовхуючись від того, що 4 байти після FF 15 у нас повинні становити одне значення форматуємо під них. А відсутні байти замінимо на операцію nop з кодом 0x90.

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 C3 14

І знову складемо 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 і використовувати інший код виклику функції, але я не встиг дослідити це питання.

Буду радий вашим відгукам і зауважень.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.