x86/x64 Platform Independent Code

Znalazłem ciekawe tricki związane ze zmianami w opcodach instrukcji między x86 a x64. Odpowiednie wykorzystanie ich pozwala na łatwe wykrycie typu architektury i tworzenie wspólnej implementacji funkcji lub kodu dla obu tych platform. Co szczególnie może być użyteczne przy różnego rodzaju shellcodach i innych takich zabawkach.

Nad takimi konstrukcjami w syringe zacząłem się zastanawiać, po tym, jak w minionym tygodniu zmieniłem pewien hack, związany z określeniem długości kodu wstrzykiwanego przy ładowaniu dll-ki do procesu. Był to tymczasowy hack, bazujący na pewnych, ogólnie znanych zachowaniach kompilatora, ale odkąd pojawiło się dużo więcej statycznych obiektów, ich kolejność została zaburzona. Nowy kod, tak jak powinno to być zrobione od początku, zawarty jest w tablicy w postaci binarnej reprezentacji opcodów docelowej funkcji.

Przy okazji tego zacząłem się zastanawiać nad obsługą również 64-bitowych procesów, teoretycznie obecny kod jest prosty, więc powinien w obu architekturach działać bez zarzutu. Ale i tak pozostaje jeszcze wiele elementów jakie należałoby naprawić, bądź zaimplementować. Na przykład LoadLibrary z kernel32.dll przy włączonym ASLR. Ale ten temat zostawiam na później.

Wszystko zaczęło się od trafienia na artykuł „Asmcodes: Platform Independent PIC for Loading DLL and Executing Commands” na blogu Odzhan-a. Gdzie autor w swoim kodzie wykorzystał ciekawą konstrukcję autorstwa innego dewelopera. Po krótkim reserachu w sieci, okazało się, że ów autor inspirował się rownież innym kodem… itd…

Konstrukcja jest dosyć prosta, a magia tkwi w szczegółach kodowania i interpretacji poszczególnych instrukcji przez procesor, które dla 64-bitowca będą trochę inne niż dla starego poczciwego x86. Moja pierwsza reakcja na kod była typowym inżynieryjnym „WTF!?”, ale po głębszej analizie i przejrzeniu manuali Intela/AMD, wszystko stało się jasne.

code:
	bits 32
	xor eax, eax
	inc eax
	xchg eax, eax
	jz x64code
 
x86code:
	bits 32
	...
 
x64code:
	bits 64
	...

Wszystko skupia się na pierwszych 3 instrukcjach, które zostaną „zakodowane” w 32-bitowych opcodach:

31 c0	xor eax, eax
40		inc eax
90		xchg eax, eax

Dla x86 działanie kodu jest modelowe, wyczyszczenie rejestru eax, jego inkrementacja i xchg lub nop, więc w wyniku nie będzie zera, nie będzie skoku, kod będzie dalej wykonywany w bieżącej ścieżce.

Dla x64 kod będzie już trochę inaczej działał, nie będzie inkrementacji, a rejestr eax zawierający 0 doprowadzi do skoku pod adres określony etykietą x64code. Stanie się tak dlatego, że procesor pracujący w trybie 64-bitowym będzie widział ten kod w zupełnie innej postaci:

31 c0	xor eax, eax
40 90	xchg eax, eax

W 64-bitrach nie ma jednobajtowych instrukcji, a takowy inc eax (0x40) stał się prefiksem REX dla operacji nop/xchg.

Wraz z wprowadzeniem nowych 8 rejestrów ogólnego przeznaczenia GPR (General Purpose Register) w 64-bitowej platformie (amd64, x86-64) ich liczba wzrosła do 16, co spowodowało pewne problemy z kodowaniem instrukcji inc/dec znajdujących się w przedziale 0x400x50. Bo jak zakodować 32 instrukcje na 16 bitach?

W x86 wystarczyły 3 bity aby zakodować 8 rejestrów. W x64 potrzebny jest jeszcze jeden. Dlatego wprowadzono prefiks REX, który dodawał nowe możliwości (bity) dla instrukcji i operandów. REX wpakowano właśnie w przestrzeń 0x40-0x47. Przez co występujące tam nieszczęsne instrukcje musiały zostać wysiedlone. Dostępne są za to wciąz w formie 2 bajtowych instrukcji kodowanych za pomocą ModR/M: FF /0 i FF /1.

W manualu AMD64, w sekcji 2.5.11 Reassigned Opcodes znajduje się tabela prezentująca wprowadzone zmiany w przypisaniach opcodów:

opcodes-reassigned

Na szczęście zmian dużo nie ma, ale ciekawe czy są dostępne jeszcze jakieś inne konstrukcje wykorzystujące fakt zmian w kodowaniu instrukcji między platformami.

Istotne może być to, że w trybie 64-bitowym domyślnie instrukcja xchg będzie operowała na 32-bitowych operandach, mimo pojawienia się prefiksu REX, bez ustawionych bitów R/W. Ale to nie jest problemem bo domyślnie 32-bitowe wartości zostaną w rejestrze 64-bitowym awansowane (??) (promotion) do 64-bitów przez dodanie 0 w górnej połówce. W powyższym kodzie i tak rejestr eax jest czyszczony na początku, więc to bez większego znaczenia. Ale istotne może być w sytuacji standardowego użycia instrukcji xchg.

I tutaj przy okazji, warto głębiej podrażyć temat instrukcji kodowanej jako 0x90, szczególnie w kontekście danej platformy.

Instrukcja xchg w wersji z rejestrami eax stała się powszechnym synonimem nieistniejącej instrukcji nop. W 64-bitowym trybie pojawiają się pewne komplikacje, o których napomknąłem wyżej. W istocie nop w 64-bitach nic nie zrobi, prócz wyczyszczenia górnych 32 bitów w rejestrze eax.

Aby rozwiązać ten problem, zdecydowano się na wprowadzenie explicit nop, rzeczywistej instrukcji, nic nie robiącej, kodowanej wielo-bajtowo, która zgodnie z dokumentami Intela (Instruction Set Reference, NOP—No Operation):

The multi-byte NOP instruction does not alter the content of the register and will not issue a memory operation.

W tym samym miejscu można znaleźć informacje o jedno-bajtowej reprezentacji:

The one-byte NOP instruction is an alias mnemonic for the XCHG (E)AX, (E)AX instruction.

Ciekawostka pojawia się w dokumentach AMD (2.5.7 NOP Instruction), bo tam jasno napisano, że w 64-bitowym trybie, 0x90 nadal pozostanie true no-operation:

Without special handling in 64-bit mode, the instruction would not be a true no-operation. Therefore, in 64-bit mode the processor treats opcode 90h (the legacy XCHG EAX, EAX instruction) as a true NOP, regardless of a REX operand-size prefix.

No i mam mała sprzeczność lub niedopowiedzenie w dokumentach Intela, bo większość materiałów w sieci mówi jasno, że w 64-bitowym trybie nop kodowany jako 0x90 nie jest już typowym aliasem na xchg eax eax.

Dokumenty zalecają używanie wielo-bajtowych instrukcji nop:

multi-byte-nop

Które oczywiście na niewspierających ich procesorach walną wyjątkiem undefined opcode.

Gdy w trybie 64-bitowym potrzeba wykonania xchg eax eax z wyczyszczeniem górnych bitów to również należy skorzystać z wersji dwu-bajtowej w formie kodowanej z ModR/M: 87 C0.

Na szczęście, ten mały rozdźwięk z 0x90 mnie nie boli, jak wyżej napisałem, dla mojego kodu to w istocie bez znaczenia.

Zastanawiam się co jeszcze ciekawego i niestandardowego można znaleźć bawiąc się różnymi instrukcjami i ich opcodami lub czytająć cegły Intela i AMD na dobranoc ;)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *