Някои хора считат самомодифициращите се програми за нещо магическо, но всъщност те са доста опростени и за да демонстрираме това, нека да покаже един пример за х86 архитектурата, написан на NASM.
Оригиналният сорс код за х86 асемблер изчисляване на факториел
Нека в началото да разгледаме стандартната програма на асемблер за изчисляване на факториел.
factorial:
push ebp
mov ebx, eax
factorial_start:
sub ebx, 1
cmp ebx, 0
je factorial_end
mul ebx
jmp factorial_start
factorial_end:
pop ebp
ret
Тук няма нищо сложно.
Самомодифициращият се факториел
В алгоритъма за изчисляване на факториела има две места, в които промяната на значенията по време на изпълнението има смисъл. Това са началното значение и множителят.
Техническите особености
Първо, самомодифициращите се програми си има своя характерна специфика. По подразбиране NASM линкерът свързва програмата без възможност за нейната по-нататъшна самостоятелна модификация, вероятно за да не се създават компютърни вируси. Поради съображения за информационната безопасност разделът .text се обозначава само за четене. За да се промени съответния флаг за този раздел с цел да се активира възможността за презапис на неговите клетки е необходимо да се използва objcopy и една малка допълнителна подпрограма.
Началните значения
В сорс кода първоначалното число се подава чрез регистъра EAX. За да може да се използва самомодифициращия се код, най-напред е необходимо в началото на тази функция да бъде включена инструкцията MOV за нулиране на регистъра EAX:
_start:
mov dword [factorial+2], 0x5
call factorial
factorial:
push ebp
mov eax, 0
Лесно може да се види, че за подаване началното значение програмата променя инструкцията mov eax. Числото нула в тази инструкция е отместено с два байта напред от самото начало на функцията factorial. Именно на мястото на тази 0 се записва значението на факториела, който трябва да бъде изчислен.
Множителят
factorial_start:
; multiply
mov ebx, 0
mul ebx
Това е кодът, който ще бъде модифициран от самата програма и ще се използва за умножението. След това трябва да съставим необходимия алгоритъм за поставяне на началното значение за изчисляване на факториела в инструкцията mov ebx, 0, неговото декрементиране и окончателно излизане от цикъла.
Инициализацията на множителя
За да се зададе множителя използваме регистъра EBX, в който е записано неговото първоначално значение и го копираме на подходящото място в инструкцията mov eax, 0, която помним, че е в самото начало на функцията factorial_start. Тоест, имаме следния код:
factorial:
...
mov dword [factorial_start+1], ebx ; init decrementer
Декрементирането на множителя
В еталонната програма логиката на събитията е следната:
- Декремнтиране на множителя
- Ако той е равен на нула, излизане
- Извършване на преход назад
В нашата самомодифицираща се програма е променено само едно нещо – декрементирането на множителя.
За да можем да осъществим това е необходимо да получим неговото текущо значение, да го намалим с единица и да го копираме обратно на същото място. Това най-лесно може да стане по следния начин:
factorial_start:
...
; decrement
mov ebx, dword [factorial_start+1]
sub ebx, 1
mov dword [factorial_start+1], ebx
Крайният резултат
След като съберем всичко изброено до тук на едно място ще получим следния сорс код:
extern printf
section .data
format: db "num: %d",10,0
section .text
global _start
_start:
mov dword [factorial+2], 0x5 ; start number
call factorial
; print result
push eax
push format
call printf
; exit
mov eax, 1
mov ebx, 0
int 80h
factorial:
push ebp
mov eax, 0
mov ebx, eax
sub ebx, 1
mov dword [factorial_start+1], ebx ; init decrementer
mov ebx, 0
factorial_start:
; multiply
mov ebx, 0
mul ebx
; decrement
mov ebx, dword [factorial_start+1]
sub ebx, 1
mov dword [factorial_start+1], ebx
; exit if at 0
; could exit at 1, but then it doesn't handle 0x2
cmp ebx, 0
je factorial_end
; loop back
jmp factorial_start
factorial_end:
pop ebp
ret
Изводи
Лично на мен самомодифициращите се програми са изключително интересни. Техният код е доста по-различен, изглежда по друг начин, доста е безпорядъчен и включва празни стойности, а схващането на самата логика на алгоритъма е много увлекателно.
Този начин за писане на код се използва в най-различни области и основно за обфускация – умишленото създаване на сорс код или директно на машинен код, който е твърде труден за разбиране от хората. Методът масово се използва за защита на лицензии и за писане на вредоносен софтуер. Мисля да използвам този подход, за да създам своя собствена програма за компресия и криптиране на данните, както и някой много интересен crackme файл.
Към тази статия има голям брой коментари, като има програмисти, които са на мнение, че този пример не е много удачен, понеже изчисляваният факториел трябва да се подава директно в програмата като константа. Предлага се кодът да бъде направен по-универсален като се добави допълнителен параметър, който да подава факториела, който ще се изчислява, чрез стека. Други програмисти контрират, че по този начин бързодействието на алгоритъма силно ще намалее, понеже ще се налага нулиране на стека, а това става чрез кеша.
По принцип, едва ли някоя съвременна операционна система ще разреши подобни директни действия без специални права, но това е друга тема.
Всичко важно от света на технологиите, директно в пощата ти.
С абонирането приемате нашите Условия и Политика за поверителност. Може да се отпишете с един клик по всяко време.
Коментирайте статията в нашите Форуми. За да научите първи най-важното, харесайте страницата ни във Facebook, и ни последвайте в Google Новини, TikTok, Telegram и Viber или изтеглете приложението на Kaldata.com за Android, iPhone, Huawei, Google Chrome, Microsoft Edge и Opera!
