Pull to refresh

Квайн на Ассемблере? Это просто

Reading time6 min
Views5.8K
Известно, что квайн (куайн) — программа, результатом работы которой является вывод текста самой программы. При этом предполагается, что обращения к файлу, содержащему программу, не происходит.

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

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

push 48 (или же push 30h),

так и с использованием директивы определения данных db

db 06Ah,030h (или же db 106,48).

Возможен также вариант db 'j0'. Во всех перечисленных случаях при компиляции получается один и тот же машинный код. Написать ассемблерный квайн, содержащий мнемоническое представление инструкций, затруднительно — по-видимому, потребуются тысячи строк программы. Поэтому основная идея, которой будем руководствоваться — получение квайна с директивами db в виде

Исходник 1

.386
.model flat,stdcall
.code
q:
db 16-ричные коды инструкций и данных
....
db 16-ричные коды инструкций и данных
end q


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

Исходник 2

; @echo off

; ml /c /coff quine.cmd
; pause
; if errorlevel 1 goto exit

; link /subsystem:windows /section:.text,RW quine.obj
; pause
; del quine.obj
; goto exit

.386
.model flat,stdcall
option casemap:none

include		\masm32\include\windows.inc

bStr		= 25						; кол-во байтов, определяемых в одной строке квайна
qLen		= len * 5 + (len / bStr + 1) * 4 + len1 + 10	; размер исходника квайна
stLen		= (qLen / 1000h + 1) * 1000h			; выделяемая в стеке память под исходник

.code

; *** Часть 1 - формирование в стеке текста исходника ***

start:		cld
		mov edx,[esp]		; ссылка вглубь kernel32.dll - для части 2
		sub esp,stLen		; выделение области в стеке
		mov edi,esp
		mov stAddr,edi
		lea esi,text
		xor ecx,ecx
		mov cl,len1
rep 		movsb			; копирование преамбулы '.386 .model flat ...'

		lea esi,start
		mov bl,bStr
cycl:		mov eax,ecx		; генерация текста. Конец строки?
		div bl
		or ah,ah
		jnz comma
		mov ax,0A0Dh		; да - перевод строки и вставка директивы db		
		stosw
		mov eax,'  bd'
		stosd
		dec edi
		jmp j1
comma:		mov al,','		; нет - вставка запятой, разделителя байтов
		stosb
j1:		mov al,'h'		; формирование 16-ричного представления байта - суффикс h
		shl eax,16
		lodsb
		mov ah,al
		and al,0Fh
		cmp al,10
		sbb al,69h
		das			; младшая половина байта
		xchg al,ah
		shr al,4
		cmp al,10
		sbb al,69h
		das			; старшая половина
		shl eax,8
		mov al,'0'		; префикс 0
		stosd
		inc ecx
		cmp ecx,len
		jb cycl

		mov ax,0A0Dh		; концовка квайна 'end q'
		stosw
		mov eax,' dne'
		stosd
		mov eax,'q'
		stosd

; *** Часть 2 - нахождение адресов API функций ***

		xor dx,dx
c2:		cmp word ptr [edx],'ZM'	; MZ-заголовок модуля kernel32?
		je c1
c3:		sub edx,10000h
		jmp c2
c1:		mov ecx,[edx+3Ch]
		cmp dword ptr [edx+ecx],'EP' ; PE-заголовок?
		jne c3

		mov ecx,[edx+ecx+78h]
		add ecx,edx		; адрес таблицы экспорта kernel32
		push ecx
		mov eax,[ecx+20h]
		add eax,edx		; адрес таблицы адресов имен функций
		xor ebx,ebx
		xor ecx,ecx
cycl2:		mov esi,[eax+4*ebx]
		add esi,edx
		lea edi,GPAname
		mov cl,len2
repe		cmpsb			; поиск строки GetProcAddress
		je found
		inc ebx
		jmp cycl2

found:		pop ecx
		mov eax,[ecx+24h]
		add eax,edx
		mov bx,[eax+2*ebx]	; ординал функции GetProcAddress
		mov eax,[ecx+1ch]
		add eax,edx
		mov ebx,[eax+4*ebx]
		add ebx,edx
		mov edi,edx		; EBX - адрес GetProcAddress
					; EDI - база модуля kernel32 (а затем - user32)


		xor ecx,ecx		; нахождение адресов остальных функций
		mov cl,tlen
		lea esi,tbl

cycl3:		cmp cl,tlen-tlen2	
		jne j2

		push ecx
		push offset DLLname
		call eax		; загрузка user32.dll с помощью вызова LoadLibrary
		pop ecx
		mov edi,eax

j2:		dec cl
		push ecx
		push [esi+4*ecx]
		push edi
		call ebx		; получение адресов функций с помощью GetProcAddress
		pop ecx
		mov [esi+4*ecx],eax

		or cl,cl
		jnz cycl3

; *** Часть 3 - вывод исходника в окно MessageBox ***

		push NULL
		call dword ptr _GetModuleHandle
		mov hInst,eax

		call dword ptr _GetCurrentThreadId

		push eax
		push NULL
		push offset hProc
		push WH_CBT			; установка хука на системные окна
		call dword ptr _SetWindowsHookEx
		mov hook,eax

		push MB_OK
		push offset capt
		push stAddr
		push 0
		call dword ptr _MessageBox	; окно с текстом квайна

		push 0
		call dword ptr _ExitProcess

; ***

hProc		proc code:dword,wParam:dword,lParam:dword	; обработчик хука
		local coord:RECT

		pusha
		cmp code,HCBT_ACTIVATE		; активация окна?
		jne exit

		push 0FFFFh
		push wParam
		call dword ptr _GetDlgItem
		mov ebx,eax			; хэндл текстового поля

		lea eax,coord
		push eax
		push ebx
		call dword ptr _GetClientRect	; координаты текстового поля
		add coord.right,20

		push NULL
		push NULL
		push WM_GETFONT
		push ebx
		mov edi,_SendMessage
		call edi			; текущий шрифт текста
		mov esi,eax

		push SW_HIDE
		push ebx
		call dword ptr _ShowWindow	; скрытие текста

		push NULL
		push hInst
		push 0FFFFh
		push wParam
		push coord.bottom
		push coord.right
		push coord.top
		push coord.left
		push WS_CHILD+WS_VISIBLE+ES_MULTILINE+ES_READONLY
		push stAddr
		push offset cname
		push WS_EX_WINDOWEDGE
		call dword ptr _CreateWindowEx	; окно EDIT вместо STATIC
						; для того, чтобы из него можно было копировать

		push NULL
		push esi
		push WM_SETFONT
		push eax
		call edi			; установка шрифта

		push hook
		call dword ptr _UnhookWindowsHookEx ; снятие хука
		popa
		xor eax,eax
		ret

exit:		popa
		push lParam
		push wParam
		push code
		push hook
		call dword ptr _CallNextHookEx	; на следующий обработчик
		ret
hProc		endp

; *** инициализированные данные

capt		db 'Quine',0			; заголовок окна
GPAname		db 'GetProcAddress',0
len2		equ $ - GPAname

GDIname		db 'GetDlgItem',0		; имена используемых функций
GCRname		db 'GetClientRect',0
SMname		db 'SendMessageA',0
SWname		db 'ShowWindow',0
CWEname		db 'CreateWindowExA',0
UWHEname	db 'UnhookWindowsHookEx',0
CNHEname	db 'CallNextHookEx',0
SWHEname	db 'SetWindowsHookExA',0
MBname		db 'MessageBoxA',0
LLname		db 'LoadLibraryA',0
GMHname		db 'GetModuleHandleA',0
GCTIname	db 'GetCurrentThreadId',0
EPname		db 'ExitProcess',0

DLLname		db 'user32.dll',0
cname		db 'EDIT',0			; класс окна

text		db '.386',13,10,'.model flat,stdcall',13,10,'.code',13,10,'q:' ; преамбула квайна
len1		equ $ - text

tbl			label dword		; таблица смещений имен функций, динамически заменяемых на адреса

_GetDlgItem		dd offset GDIname	; функции из user32.dll
_GetClientRect		dd offset GCRname
_SendMessage		dd offset SMname
_ShowWindow		dd offset SWname
_CreateWindowEx		dd offset CWEname
_UnhookWindowsHookEx	dd offset UWHEname
_CallNextHookEx		dd offset CNHEname
_SetWindowsHookEx	dd offset SWHEname
_MessageBox		dd offset MBname

tbl2			label dword
_LoadLibrary		dd offset LLname	; функции из kernel32.dll
_GetModuleHandle	dd offset GMHname
_GetCurrentThreadId	dd offset GCTIname
_ExitProcess 		dd offset EPname
tlen2			equ ($-tbl2) / 4

tlen			equ ($-tbl) / 4		; размер таблицы

len		equ $ - start			; размер кода

; *** неинициализированные данные, в квайн не попадающие

hInst		dd ?				; хэндл модуля
hook		dd ?				; хэндл хука
stAddr		dd ?				; адрес текста квайна в стеке

end start

:exit


Принцип работы ясен из приведенных в тексте комментариев. Отметим только, что программа состоит из трех частей. В первой части в стеке формируется текст квайна (т.е. исходник 1). Количество байтов, определяемых одной директивой db (следовательно, и ширина строк квайна), задается константой bStr — она установлена равной 25-и. Поскольку во время компиляции не импортируются какие-либо API функции, то во второй части динамически определяются адреса ряда функций. Вначале это адрес функции GetProcAddress, получаемый трассировкой таблицы экспорта модуля kernel32.dll. Затем — адреса остальных функций, получаемых с помощью GetProcAddress. Наконец, в третьей части осуществляется вывод квайна в окно MessageBox. Поскольку по умолчанию выводимый текст статический и его нельзя куда-либо скопировать, дополнительно устанавливается хук, позволяющий копировать текст из окна. Возможны другие варианты третьей части квайна — например, с помощью создания очереди сообщений и написания оконной функции.

Итак, исходник 2 (не являющийся квайном) необходимо сохранить в пакетном файле quine.cmd и запустить его — получается исполняемый файл quine.exe. Далее запускается quine.exe, и из появившегося окна следует, выделив мышью, скопировать текст в файл quine.asm. Исполняемый файл можно удалить. Таким образом, в quine.asm — полноценный квайн (см. исходник 1). Транслируется он с помощью

ml /c /coff quine.asm

а линкуется с помощью

link /subsystem:windows /section:.text,RW quine.obj

(в секцию кода должна быть разрешена запись). Размер len секции кода составил 820 байт.

Отладку программы я производил турбо дебаггером, но при этом дополнительно включал строку

invoke ExitProcess,0

перед точкой входа start (можно взять любую другую функцию), и строки

include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

Иначе дебаггер зависает — по-видимому, из-за отсутствия таблицы импорта в исполняемом файле.
Tags:
Hubs:
Total votes 43: ↑18 and ↓25-7
Comments8

Articles