2009-03-14:

OS X, Objective C i RE

macosx:objc:easy:re
Nadszedł dzień w którym rzuciłem w końcu okiem na programowanie aplikacji niekonsolowych na Mac'a. Aplikacja na Maca zazwyczaj tworzy się przy użyciu języka Objective C (z którym jeszcze styczności nie miałem) oraz API Cocoa (OSX'owski odpowiednik WinAPI; kiedyś był jeszcze Carbon). Z punktu widzenia programisty składnia Objective C bardzo mi się spodobała, ale przyznaje szczerze, że Objective C z punktu widzenia RE jest jeszcze ciekawsze ;>

Najpierw krótki przykładowy programik w Objective C, który, korzystając z klas od HTTP z Cocoa, ściąga główną stronkę mojego bloga do pewnego bufora:

#include <cocoa/cocoa.h>

char buffer[1024 * 1024];

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

 NSURLRequest *theRequest =
   [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://gynvael.coldwind.pl/"]
                 cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
                 timeoutInterval:20.0];

 NSURLResponse *theResponse= NULL;
 NSData *theData =
   [NSURLConnection sendSynchronousRequest:theRequest
                    returningResponse:&theResponse
                    error:NULL];

 [theData getBytes:buffer length:sizeof(buffer)];

 puts(buffer);

return 0;
}



Jeżeli ktoś wcześniej nie miał styczności z ObjC, to kilka słów wyjaśnienia (jeżeli ktoś natomiast pisał w ObjC, to proponuje przeskoczyć ten fragment). Standardowa składnia w C++ (dla kontrastu) wygląda następująco:

PtrNaObiekt->Metoda(Parametr1, Parametr2, Parametr3);
Klasa::MetodaStatyczna(Parametr1, Parametr2, Parametr3);


Analogiczne kontrukcje w ObjC wyglądają tak:

[PtrNaObiekt Metoda:Parametr1 NazwaParametru2:Parametr2 NazwaParametru3:Parametr3];
[Klasa MetodaStatyczna:Parametr1 NazwaParametru2:Parametr2 NazwaParametru3:Parametr3];


Jak widać, sprawa wygląda dość podobnie. Dodatkową sprawą są stringi z @ przed nimi, np. @"ala ma kota" - taki zapis powoduje powstanie obiektu typu CFString (czy tam NSString), który opisuje dany string (ot taki podobny twór do klasy string w C++ z własną notacją w kodzie).

Tak że na początku tworzone jest zapytanie (theRequest), które jest obiektem klasy NSURLRequest. Ów zapytanie tworzone jest przez statyczną metodę requestWithURL, która przyjmuje 3 parametry: URL, wartość określającą jak cache ma się zachowywać (cachePolicy), oraz informację po jakim czasie ma nastąpić timeout (timeoutInterval). URL podawany jest jako obiekt klasy NSURL, a który to obiekt tworzony jest przez statyczną metodę URLWithString na podstawie stringu typu CFString czy tam NSString.
Po utworzeniu zapytania następuje wywołanie kolejnej statycznej metody, tym razem z klasy NSURLConnection, o nazwie sendSynchronousRequest, która to przyjmuje m.in. zapytanie, a zwraca obiekt klasy NSData, będący opakowaniem danych (pewnie coś jak vector z C++ or sth).
Z tego ostatniego obiektu, korzystając z metody (tym razem nie statycznej) getBytes, można wydobyć dane.
Dane są następnie kopiowane na stdout aż to pierwszego \0 (zignorujmy przypadek gdy danych jest dokładnie 1MiB lub więcej).

Jako ciekawostkę (język ObjC jest dla mnie dość egzotyczny, dlatego oczywistości dla programistów tego języka są dla mnie ciekawostkami ;>) podam że metody statyczne są tu nazywane metodami klasy, a metody zwykłe - metodami obiektu, trzeba przyznać że jest w tym dużo sensu.

Kompilacja powyższego z linii poleceń (a jak!) odbywa się w następujący sposób:

gcc test.m -Wl,-framework,cocoa -o test

Oczywiście równie dobrze można kompilować z poziomu XCode (IDE dostarczane przez Apple).

Jest programik, jest on skompilowany, więc można go wrzucić w nasz ulubiony disassembler, i zobaczyć co tam ciekawego w środku siedzi.

Najciekawsz moim zdaniem są następujące cechy reversowanej aplikacji:
1) Funkcje mają identyczny prefix jak w przypadku MinGW GCC na Windowsie - underscode _, np _main, _func.
2) Prolog funkcji zawiera, oprócz tworzenia ramki stosu, również ustalanie EIP:
call $+5
pop ebx

3) Wszystkie dalsze odwołania do danych odbywają się z udziałem rejestru ebx, czyli są adresowane względem pozycji instrukcji pop ebx - jest to dość osobliwe i rzadko spotykane w czymś innym niż shellcode'ach.
4) Jest około 16 (!) sekcji (__text, __cstring, __literal8, __data, __dyld, __cfstring, __bss, __common, __message_refs, __cls_refs, __module_info, __image_info, __pointers, __jump_table, __LINKEDIT_hidden, ABS, do tego HEADER i coś oznaczonego UNDEF)
5) W sekcji __cstring siedzą stringi (niespodzianka!) ASCIIZ (aka C string). Co ciekawe, oprócz stringów ASCIIZ użytych bezpośrednio w programie, znajdują się tam również nazwy klas (np. NSURLConnection), nazwy metod (np. requestWithURL:cachePolicy:timeoutInterval:) oraz stringi opisywane przez obiekty CFString.
6) W sekcji __cfstring znajdują się obiekty typu CFString (ich struktura jest prosta - adres klasy CFConstantStringClassReference, flagi, offset na string w sekcji __cstring oraz długość ów stringa, każde pole po 4ry bajty)
7) "buffer" (patrz kod) trafił do sekcji __common
8) W sekcji __cls_refs znajdują się pointery na stringi z nazwami klas:
__cls_refs+00: dd offset __cstring:"NSURLRequest"
__cls_refs+04: dd offset __cstring:"NSURL"
__cls_refs+08: dd offset __cstring:"NSURLConnection"

Co ciekawe, po odpaleniu programu te pointery są zamieniane na pointery na deskryptory klas - działa to zupełnie jak IAT w PE na Windowsie.
9) W sekcji __message_refs są pointery na nazwy metod, które, analogicznie jak wyżej, są zamieniane na pointery na metody przez loader.
10) Sekcja __jump_table jest rwx, i podczas ładowania exeka bądź działania programu faktycznie jest nieznacznie modyfikowana
11) Wywołania metod klas/obiektów są zrealizowane inaczej niż w C++. Dla przypomnienia, w C++ obowiązuje thiscall - pointer na obiekt ląduje w ecx, po czym metoda jest wywoływana jak normalna funkcja. Natomiast w przypadku ObjC mamy do czynienia z wywołaniem funkcji objc_msgSend, która przyjmuje przynajmniej dwa parametry: pointer na deskryptor klasy/obiekt, oraz pointer na metodę. Dodatkowymi parametrami mogą być parametry metody (bez ich nazw ;D). Ów funkcja jest odpowiedzialna za wywołanie metody z parametrami, ale tak na prawdę stoi za nią cała historia (którą opowiem innym razem, póki co zainteresowanych odsyłam tutaj i tutaj). Co ciekawe, kompilator korzysta w kodzie z pointera na pointer, który następnie 'resolvuje', przez co kod jest trochę mało czytelny:
__text:00001EBA                 lea     eax, dword_1011CD[ebx]
__text:00001EC0                 mov     eax, [eax]
__text:00001EC2                 mov     edx, eax
__text:00001EC4                 lea     eax, dword_1011B9[ebx]
__text:00001ECA                 mov     ecx, [eax]
__text:00001ECC                 mov     dword ptr [esp+38h+var_28], 0
__text:00001ED4                 lea     eax, [ebp+var_14]
__text:00001ED7                 mov     [esp+38h+var_2C], eax
__text:00001EDB                 mov     eax, [ebp+var_10]
__text:00001EDE                 mov     [esp+38h+var_30], eax
__text:00001EE2                 mov     [esp+38h+var_34], ecx
__text:00001EE6                 mov     [esp+38h+var_38], edx
__text:00001EE9                 call    _objc_msgSend

Nieczytelność objawia się nie posiadaniem informacji o tym co jest wywoływane, a powodowana jest pointerem na pointer który jest relatywny względem ebx, a to wystarczy by nawet IDA się zgubiła - mówił o tym m.in. Charlie Miller na BH2008 w Japonii podczas prelekcji 'Owning the Fanboys: Hacking Mac OS X', zaproponował on plug-in do IDA który rozwiązuje problem. Jeżeli kogoś nie zadowala plug-in, to poniżej znajduje się skrypt IDC, który stworzyłem w ramach odkrywania koła na nowo (nie ręczę za jego stabilność):
#include <idc.idc>
static main(void)
{
 auto Start, Stop, i, RelAddr;
 auto OldAddr, NewAddr;

 Start = ScreenEA();
 if(Start == BADADDR)
 {
   Message("Invalid address");
   return;
 }

 Stop = FindFuncEnd(Start);
 if(Stop == BADADDR)
 {
   Message("Not in function or invalid address");
   return;
 }  

 Start = GetFunctionAttr(Start, FUNCATTR_START);

 Message("Func <%x, %x>...\n", Start, Stop);

 RelAddr = 0;

 for(i = Start; i < Stop; i = ItemEnd(i))
 {
   if(GetMnem(i) == "call" && Byte(i) == 0xE8 && Dword(i+1) == 0)
   {
     RelAddr = ItemEnd(i);
     Message("RelAddr found (%x)...\n", RelAddr);
   }

   if(RelAddr != 0 && GetMnem(i) == "lea" && GetOriginalByte(i) == 0x8D && (GetOriginalByte(i+1) & 0xC7) == (0x83 & 0xC7))
   {
     OldAddr = (GetOriginalByte(i+2) | (GetOriginalByte(i+3) << 8) | (GetOriginalByte(i+4) << 16) | (GetOriginalByte(i+5) << 24));
     NewAddr = OldAddr + RelAddr;
     Message("Fixing opcode at %x (%x -> %x)...\n", i, OldAddr, NewAddr);
     PatchByte(i+1, GetOriginalByte(i+1) ^ 0x86); // lea Y, [X]
     PatchDword(i+2, NewAddr);

     // To restore uncoment this
     //PatchByte(i+1, GetOriginalByte(i+1));
     //PatchDword(i+2, OldAddr);
     
     // Check for offsets to strings
     if(GetStringType(Dword(NewAddr)) != 0xffffffff)
       MakeComm(i, "->-> \"" +  GetString(Dword(NewAddr), -1, GetStringType(Dword(NewAddr))) + "\"");
     
   }
 }

 AnalyzeArea(MinEA(), MaxEA());
}

Powyższy skrypt odpala się dla jednej funkcji. Przykładowo, wklejony wcześniej fragment deadlistingu po uruchomieniu skryptu wygląda następująco:
__text:00001EBA                 lea     eax, off_103018 ; ->-> "NSURLConnection"
__text:00001EC0                 mov     eax, [eax]
__text:00001EC2                 mov     edx, eax
__text:00001EC4                 lea     eax, off_103004 ; ->-> "sendSynchronousRequest:returningResponse:error:"
__text:00001ECA                 mov     ecx, [eax]
__text:00001ECC                 mov     dword ptr [esp+38h+var_28], 0
__text:00001ED4                 lea     eax, [ebp+var_14]
__text:00001ED7                 mov     [esp+38h+var_2C], eax
__text:00001EDB                 mov     eax, [ebp+var_10]
__text:00001EDE                 mov     [esp+38h+var_30], eax
__text:00001EE2                 mov     [esp+38h+var_34], ecx
__text:00001EE6                 mov     [esp+38h+var_38], edx
__text:00001EE9                 call    _objc_msgSend


To by było na tyle na pierwszy raz. Pewnie w najbliższym czasie jeszcze coś klepne o tej egzotycznej (dla mnie) platformie jaką jest OS X.

P.S. W komentarzach pod postem o automagicznej liście funkcji pojawiło się kilka nowych propozycji jak to zrealizować. Warto rzucić okiem ;>

Comments:

2009-03-14 14:34:58 = bw
{
prawie jak stare wirki, ale nawet tam nie liczyli delty w kazdej funkcji, musial to projektowac jakis oldskoolowy virii writer :), dodac jeszcze szyfrowanie do kazdorazowego offsetu

lea eax,[ebx+fake_address ^ 0xABBA]
...
xor eax,[ebx+fake_address+ecx*4+polozenie_0xABBA]

i bylaby niezla sieka

o gyn mam pomysl, wez napisz o jakichs protektach na mac-a :), gadalem z jednym gosciem i on mowi, ze nie zna zadnego protekta na maca, jakos mi sie nie chce wierzyc, zwlaszcza teraz jak jada na intelach, ze nikt nie zrobil do binarek jakiegos cuda
}
2009-03-17 02:06:27 = Gynvael Coldwind
{
@bw
Tyaaa ;> Też sądzę że jakiś virii writer przy tym siedział haha ;>
Anyway, ciekawy scheme szyfrujący, przyznaje że nie spotkałem się wcześniej ;>

Protektory na Mac'ach mówisz. Wiesz co, oprócz UPX'a (prawie jak protektor) to chyba nie widziałem nic co by pakowało Mach-O. Ale szczerze mówiąc, to się i za dużo nie rozglądałem. Rzucę okiem na temat i coś napiszę ;>
}
2010-05-21 18:20:59 = neltam
{
Z technicznego punktu widzenia programik, który napisałeś powoduje wyciek pamięci. Aby tego uniknąć trzeba stworzyć NSAutoreleasePool i potem pod koniec działania programu wywalić ten obiekt [pool drain];. Wiem, że się czepiam, ale Obj-C ma specjalną wartość, która zastępuje NULL przy przypisaniu do wskaźnika na obiekt zera: nil. Wszystkie nazwy klas muszą być obecne w binarce, gdyż obj-c wymaga runtime'a (i jest dynamicznie typowany). Ciekawostką jest też to, że do klas możemy dodawać własne metody (nawet jeśli nie mamy dostępu do kodu danej klasy)
}
2010-09-30 09:33:29 = Dab
{
Nie tylko dodawać -- możemy też podmieniać istniejące metody. Dlatego też Obj-C działa tak wolno -- nie ma możliwości inlinowania czegokolwiek, bo wszystko może być dynamicznie zmienione.
}

Add a comment:

Nick:
URL (optional):
Math captcha: 6 ∗ 5 + 2 =