Lubię babrać się w low levelu, to zarówno moja pasja jak i praca. Jestem w trakcie testowania nowego exe protectora i jak to zwykle bywa – testuje nim co popadnie, wliczając wszelkie nietypowe pliki w formacie PE, w tym pliki z różnych kompilatorów i środowisk programistycznych jak np. DMD, Lazarus / FPC, PureBasic, a nawet takie antyki jak WATCOM.
Różne kompilatory stosują całą mase magicznych zabiegów, aby czasami nieświadomie uniemożliwić zabezpieczenie wygenerowanego pliku PE EXE, a ja staram się to ładnie obsłużyć, ominąć, hookować etc.
Dzisiaj przysiadłem sobie do kompilatora języka wprost od Google, czyli – Go.
…i zdębiałem. Ściągnąłem paczkę v1.3.1 dla Windows w wersji 32 bitowej.
O ile protector poradził sobie bez zająknięcia z testowym zabezpieczeniem pliku kompilatora – go.exe, o tyle nie byłbym sobą jeśli bym nie zajrzał do środka, jak strukturalnie zbudowany jest plik z Go (biorąc pod uwagę, że kompilator Go zbudowany jest ze źródeł w C oraz samym języku Go).
A w środku szalone lata 90 :), spójrzmy na to razem:
1. Mania używania CLD
CLD to instrukcja procesora ustawiająca flagę kierunku wykonywania operacji takich jak rep stosb, rep movsd, scasb, jej zresetowanie powoduje, że operacje te wykonują się bajt po bajcie w kolejności rosnącej, jej ustawienie powoduje, że operacje te wykonują się odwrotnie (czyli od końca do przodu).
Problem w tym, że na systemach Windows ta flaga jest domyślnie już wyzerowana, a funkcje WinApi nigdy jej nie ustawiają, rzadkością są w ogóle przypadki, że kiedykolwiek jest ona ustawiana instrukcją STD, chyba jedynie w jakichś egzotycznych przykładach napisanych w assemblerze.
W binarce go.exe znajdziemy tego całą masę:
.text:00473CF8 call runtime_emptyfunc
.text:00473CFD cld
.text:00473CFE call runtime_check
W 1 bloku kodu (a co, kto bogatemu zabroni):
.text:004BCB1E lea esi, off_742838
.text:004BCB24 lea edi, [esp+88h+var_80]
.text:004BCB28 cld
.text:004BCB29 movsd
.text:004BCB2A movsd
.text:004BCB2B lea esi, off_740DF8
.text:004BCB31 lea edi, [esp+88h+var_78]
.text:004BCB35 cld
.text:004BCB36 movsd
.text:004BCB37 movsd
.text:004BCB38 mov [esp+88h+var_70], 0FFFFFFFFh
Jeszcze tutaj:
.text:00650751 lea esi, [esp+68h+var_44]
.text:00650755 lea edi, [esp+68h+var_24]
.text:00650759 cld
.text:0065075A call sub_475CA0
I w całej masie kodu wszędzie CLD. Co ciekawe nie znalazłem w binarce ani jednej instrukcji STD, która sprawiałaby, że trzeba użyć CLD 😉
2. Undefined behaviour
W assemblerze znajdziemy instrukcje, których znaczenia nie zna nawet taki wymiatacz assemblera jak Rysio z Klanu. Wśród nich znajdziemy tajemniczą instrukcję UD2, lata temu stosowałem ją w metodach antydebug ze względu na to, że deasemblery w popularnych debuggerach nie wiedziały co z nią robić. Obecnie jest udokumentowana jako celowo nieprawidłowa instrukcja do testowania (sprytnie).
Jej zastosowanie w kodzie generowanym w nowoczesnych kompilatorach praktycznie nie istnieje. Na co komuś instrukcja, która niczego nie robi (oprócz linuxiarzy)? W kodzie kompilatora Go znalazła zastosowanie:
.text:004BC84B loc_4BC84B: ; CODE XREF: path_filepath_volumeNameLen+1BAj
.text:004BC84B call runtime_panicindex
.text:004BC850 ud2
.text:004BC852 ; ---------------------------------------------------------------------------
.text:004BC852
.text:004BC852 loc_4BC852: ; CODE XREF: path_filepath_volumeNameLen+1C8j
.text:004BC852 ; path_filepath_volumeNameLen+1CFj
.text:004BC852 mov ebp, 1
.text:004BC857 jmp short loc_4BC833
.text:004BC859 ; ---------------------------------------------------------------------------
.text:004BC859
.text:004BC859 loc_4BC859: ; CODE XREF: path_filepath_volumeNameLen+198j
.text:004BC859 call runtime_panicindex
.text:004BC85E ud2
.text:004BC860 ; ---------------------------------------------------------------------------
.text:004BC860
.text:004BC860 loc_4BC860: ; CODE XREF: path_filepath_volumeNameLen+184j
.text:004BC860 ; path_filepath_volumeNameLen+18Bj
.text:004BC860 mov ebp, 1
.text:004BC865 jmp short loc_4BC7EF
.text:004BC867 ; ---------------------------------------------------------------------------
.text:004BC867
.text:004BC867 loc_4BC867: ; CODE XREF: path_filepath_volumeNameLen+172j
.text:004BC867 call runtime_panicindex
.text:004BC86C ud2
Jest ona tu zastosowana jako bariera, upewniająca, że po runtime_panicindex nic nie zostanie wykonane. Tak dla picu i marnotrastwa miejsca. To tak jakby po ExitProcess() dać dla bajery int 3.
3. Optymalizacyjne potworki
Są takie instrukcje, których manuale zarówno Intela jak i AMD rekomendują omijać szerokim łukiem. Dlaczego? Bo to archaiczne twory z początków procesorów 386, które przetrwały na liście instrukcji, jednak nikt ich już od tamtych czasów nie optymalizował w silikonie i nikt ich nie używa, oprócz paru szalonych programistów assemblera, którzy optymalizują kod pod względem rozmiaru.
Jedną z takich instrukcji jest xchg, która wymienia zawartość 2 rejestrów, od czasów MS-DOS nie widziałem, żeby jakikolwiek kompilator generował tą instrukcję w kodzie (oprócz ręcznych wstawek w assemblerze). A tutaj mamy tego całą masę:
.text:004BC7C4 xchg eax, ebp
.text:004BC7C5 cmp al, 0
.text:004BC7C8 xchg eax, ebp
.text:004BC7C9 jz loc_4BC86E
.text:004BC7CF inc eax
.text:004BC7D0 cmp eax, ecx
.text:004BC7D2 jnb loc_4BC867
.text:004BC7D8 lea ebp, [edx+eax]
.text:004BC7DB movzx ebp, byte ptr [ebp+0]
.text:004BC7DF xchg eax, ebp
.text:004BC7E0 cmp al, 5Ch
.text:004BC7E3 xchg eax, ebp
.text:004BC7E4 jz short loc_4BC860
.text:004BC7E6 xchg eax, ebp
.text:004BC7E7 cmp al, 2Fh
.text:004BC7EA xchg eax, ebp
.text:004BC7EB jz short loc_4BC860
.text:004BC7ED xor ebp, ebp
Na pohybel manualom :P, już nawet nie mówię, że ten kod wygląda tak biednie, że mam ochotę dać darowiznę, nawet niewprawiony programista assemblera lepiej by to napisał.
Kolejny:
.text:00473F19 mov eax, offset runtime_call16
.text:00473F1E jmp eax
Podpowiem tym razem: jmp runtime_call16 i jesteście 2 bajty do przodu, niczego nie tracąc, a wręcz zyskując, jmp w tym wypadku to relatywny skok, więc wynikowy plik PE EXE będzie jeszcze mniejszy o rozmiar relokacji używany w instrukcji mov eax,adres_w_pamieci.
I znowu:
.text:0066A88E and ebx, 7
.text:0066A891 cmp ebx, 0
.text:0066A894 jz short loc_66A89F
Każdy lamer zaczynający programować w assemblerze później czy prędzej dowie się, że instrukcje takie jak and, or, xor ustawiają również flagi w zależności od wyniku wykonanej operacji, nie trzeba dodatkowo stosować porównania do sprawdzenia flagi.
Go lubi nadmiarowy kod:
.text:005FEE6C xor ebx, ebx
.text:005FEE6E cmp eax, ebx
.text:005FEE70 jz short loc_5FEEA4
Komentarz? 3 instrukcje do sprawdzenia czy rejestr EAX zawiera wartość 0. Na całym świecie kompilatorowym załatwiają to 2 instrukcje test eax,eax lub cmp eax,0 + skok.
4. Na pohybel Windowsowi
Nikt o zdrowych zmysłach nie korzysta w produkcyjnym kodzie ze struktur takich jak np. PEB czy TEB, gdyż wskutek ewolucji systemów operacyjnych, ich pola zawsze mogą się zmienić. Nikt oprócz kompilatora go.exe, który radośnie wykorzystuje go do śledzenia stosu i w razie potrzeby zwiększania jego rozmiaru:
.text:00473E10 mov ecx, large fs:14h
.text:00473E17 mov ebx, [ecx+4]
.text:00473E1D mov esi, [ebx]
Kto o zdrowych zmysłach i wbrew dokumentacji, która wyraźnie zaznacza, że struktury te mogą się zmienić w kolejnych wersjach systemu operacyjnego decyduje się na korzystanie z nich? Chyba ktoś, kto nigdy swojego oprogramowania nie testował na 2 różnych wersjach systemu Windows, albo nie zakłada, że będzie on musiał działać na innym OS.
5. Mamy swój standard
W świecie Windows istnieje określony sposób wywoływania funkcji WinApi – określany mianem stdcall, który zakłada, że kolejne parametry są wrzucane na stos i następuje wywołanie zadanej funkcji. Seria instrukcji push zakończona instrukcją call. Proste? Za proste! Skomplikujmy to. Oto jak wygląda wywoływanie funkcji WinApi w go.exe:
.text:00458C33 mov eax, SetConsoleCtrlHandler
.text:00458C39 mov [esp+14h+var_14], eax
.text:00458C3C mov eax, 2
.text:00458C41 mov [esp+14h+var_10], eax
.text:00458C45 mov eax, offset runtime_ctrlhandler
.text:00458C4A mov [esp+14h+var_C], eax
.text:00458C4E mov eax, 1
.text:00458C53 mov [esp+14h+var_8], eax
.text:00458C57 call runtime_stdcall
Czyli istnieje sobie wrapper o sweetaśnej nazwie runtime_stdcall(), któremu przekazywana jest funkcja jako wskaźnik, parametry, a wrapper sam wywołuje instrukcję call. Po co? Kto bogatemu zabroni!
Wnioski
Pierwszy jest taki, że jestem cholernie zawiedziony jakością kodu Go, jak widać można mieć górę pieniędzy, a i tak być technologicznie zacofanym do epoki kamienia łupanego.
Wiem, że w dobie nowych procesorów Intel Core i7 mało kto zwraca uwagę na takie detale, jednak nie wiem czy chciałbym aby moje oprogramowanie i algorytmy były kompilowane do takiej formy jaką obecnie oferuje najnowszy kompilator Go. To się po prostu nie dodaje jak mówi Mariusz Max Kolonko…
Na koniec jeszcze dump z programu Hello World w Go:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
.text:00401000 main_main proc near ; CODE XREF: main_main+1Aj
.text:00401000 ; runtime_main+119p
.text:00401000 ; DATA XREF: runtime_sighandler+42o
.text:00401000 ; .text:off_4CFD0Co
.text:00401000
.text:00401000 var_40 = dword ptr -40h
.text:00401000 var_3C = dword ptr -3Ch
.text:00401000 var_38 = dword ptr -38h
.text:00401000 var_1C = dword ptr -1Ch
.text:00401000 var_18 = dword ptr -18h
.text:00401000 var_14 = byte ptr -14h
.text:00401000 var_C = dword ptr -0Ch
.text:00401000 var_8 = dword ptr -8
.text:00401000 var_4 = dword ptr -4
.text:00401000
.text:00401000 mov ecx, large fs:14h
.text:00401007 mov ecx, [ecx+0]
.text:0040100D cmp esp, [ecx]
.text:0040100F ja short loc_40101C
.text:00401011 xor edi, edi
.text:00401013 xor eax, eax
.text:00401015 call runtime_morestack_noctxt
.text:0040101A jmp short main_main
.text:0040101C ; ---------------------------------------------------------------------------
.text:0040101C
.text:0040101C loc_40101C: ; CODE XREF: main_main+Fj
.text:0040101C sub esp, 40h
.text:0040101F lea ebx, off_4B1260
.text:00401025 mov ebp, [ebx]
.text:00401027 mov [esp+40h+var_1C], ebp
.text:0040102B mov ebp, [ebx+4]
.text:0040102E mov [esp+40h+var_18], ebp
.text:00401032 lea edi, [esp+40h+var_14]
.text:00401036 xor eax, eax
.text:00401038 stosd
.text:00401039 stosd
.text:0040103A lea ebx, [esp+40h+var_14]
.text:0040103E cmp ebx, 0
.text:00401041 jz short loc_401096
.text:00401043
.text:00401043 loc_401043: ; CODE XREF: main_main+98j
.text:00401043 mov [esp+40h+var_C], ebx
.text:00401047 mov [esp+40h+var_8], 1
.text:0040104F mov [esp+40h+var_4], 1
.text:00401057 mov [esp+40h+var_40], offset dword_48CEE0
.text:0040105E lea ebx, [esp+40h+var_1C]
.text:00401062 mov [esp+40h+var_3C], ebx
.text:00401066 call runtime_convT2E
.text:0040106B mov edx, [esp+40h+var_C]
.text:0040106F lea ebx, [esp+40h+var_38]
.text:00401073 mov esi, ebx
.text:00401075 mov edi, edx
.text:00401077 cld
.text:00401078 movsd
.text:00401079 movsd
.text:0040107A mov [esp+40h+var_40], edx
.text:0040107D mov ebx, [esp+40h+var_8]
.text:00401081 mov [esp+40h+var_3C], ebx
.text:00401085 mov ebx, [esp+40h+var_4]
.text:00401089 mov [esp+40h+var_38], ebx
.text:0040108D call fmt_Println
.text:00401092 add esp, 40h
.text:00401095 retn
.text:00401096 ; ---------------------------------------------------------------------------
.text:00401096
.text:00401096 loc_401096: ; CODE XREF: main_main+41j
.text:00401096 mov [ebx], eax
.text:00401098 jmp short loc_401043
.text:00401098 main_main endp
Jeśli i wy doceniacie piękno assemblera i gardzicie lipnym kodem wpiszcie w komentarzach waszą wersję tej funkcji, najlepszy wpis czeka nagroda – pierwszy plakat formatu PE (bo już nie mam go gdzie powiesić). Nie spodziewam się rewelacji z waszej strony, ale kto wie 😉