niedziela, 3 lutego 2013

Hook EAT/IAT: Poznajemy strukture dll

Jest to pierwsza część serii. Głównym celem będzie podsłuchanie ruchu gry oraz napisanie prostego bota odpowiadającego na zdarzenia zachodzące w grze.

W tej części poznamy budowę dynamicznej biblioteki współdzielonej "od środka" i podstawy do założenia hooka na tablicę importów/eksportów tejże biblioteki. Mam nadzieję, że uda mi się osiągnąć cel.

Naszym celem w pierwszej części będzie podpięcie się pod proces gry i założenie z "wewnątrz" hooka na odpowiednie funkcje, tj.:
recv();
send();
zawarte w Ws2_32.dll.

Zanim zaczniemy myśleć o tym trzeba poznać budowę oraz wyciągnąc odpowiednie adresy z wnętrza biblioteki dynamicznej.

Tutaj niestety kochany microsoft nie służy zbyt wielką pomocą odnośnie swoich struktur. W większości jedynie minimalne opisy. Po małych poszukiwaniach znalazłem co będzie nam potrzebne.

Na początku zaczynamy od wyciągnięcia uchwytu do DLL'ki. Prosta sprawa używamy w tym celu jedynie funkcji GetModuleHandle(Ex).


HMODULE dllHandler = GetModuleHandle(L"msvcrt.dll");
if(dllHandler == NULL)
    cout << "DLL Handler couldn't be resolved.\n";


Kolejnym krokiem będzie wyciągnięcie struktury nagłówka tzw. "PE Header". W tym celu będziemy potrzebowali adres pod który załadowano naszą bibliotekę w module exeka. Niestety struktura która jest nam potrzebna nie jest praktycznie opisana. Najlepszy opis jaki udało mi się znaleźć można podejrzeć tutaj:

IMAGE_DOS_HEADER

Na szczęście główna struktura która będzie nam potrzebna jest opisana przez microsoft:

IMAGE_NT_HEADER

PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((BYTE*)dllHandler + ((PIMAGE_DOS_HEADER)dllHandler)->e_lfanew);
if(ntHeader->Signature != IMAGE_NT_SIGNATURE)
    cout << "NT Header signature error.\n"

Koncepcja wyciągnięcia adresu jest prosta. Adres bazowy + offset. Wiemy, że e_lfanew jest adresem początku wczytanej biblioteki w module naszego exeka. Dla pewności sprawdzamy sygnature struktury, żeby sprawdzić czy trafiliśmy pod dobry adres. Następnie musimy dostać się do kolejnej(na szczęście ostatniej) struktury, która zawiera to czego szukamy. Kolejny raz bez odpowiedniej dokumentacji...


PIMAGE_EXPORT_DIRECTORY ied = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)dllHandler + ntHeader->OptionalHeader.DataDirectory[0].VirtualAddress);

DataDirectory jest to tablica 2 elementowa zawierajaca wskaźniki na EAT/IAT, czyli tablicę eksportów/importów funkcji danej biblioteki. W naszym wypadku 0 oznacza EAT.

Mając już odpowiednią strukturę możemy wyciągnąć w końcu informacje.

PVOID names = (PVOID)((BYTE*)dllHandler + ied->AddressOfNames);
PVOID address = (PVOID)((BYTE*)dllHandler + ied->AddressOfFunctions);
vector<string>vnames;
vector<DWORD*>vaddress;
for(int i=0;i<ied->NumberOfNames;i++)
{
    vnames.push_back((char*)((BYTE*)dllHandler+((DWORD*)names)[i]));
    vaddress.push_back((DWORD*)dllHandler+((DWORD*)address)[i]);
}
for(int i=0;i<vnames.size();i++)
{
    if(vnames[i].compare("printf")==0)
    {
        cout << vnames[i] << " " << vaddress[i] << '\n';
        myprint func = (myprint)GetProcAddress(dllHandler,"printf");
        func("hello world");
    }
}

Z opisu IED wynika, że adresy funkcji sa adresami relatywnymi. Więcej info czym dokładnie są takie adresy można znaleźć tutaj: RVA/VA

Skoro są to adresy relatywne tak więc w pętli nadal musimy korzystać z naszego wzoru Adres bazowy + offset. GetProcAddress jest użyty w celu sprawdzenia czy udało sie znaleźć prawdziwy adres wewnątrz naszego procesu danej funkcji printf. "myprint" jest tutaj definicją wskaźnika na prototyp funkcji printf.

typedef int (*myprint)(const char* format, ...);
Tutaj rodzi się problem. O ile funkcja GetProcAddress znajduje poprawny adres funkcji printf, o tyle nasz wyciągnięty adres różni się od niego. Zapewne jest to związane z tym, że adresy przez nas otrzymane nie są adresami realnymi, a relatywnymi.

Edit 1.

Tak więc po kolejnych paru godzinach kombinowania doszedłem w końcu do rozwiązania problemu. Okazało się, że niepoprawnie wyciągałem adresy z tablicy, a było to spowodowane nieznajomością jej struktury ;) (Kolejny raz dziękujemy microsoftowi za dokumentację...)

Robiłem coś takiego

address = reinterpret_cast<PDWORD>(dllBase + baseFunctionsAddress[i]);

kiedy poprawnym rozwiązaniem było

address = reinterpret_cast<PDWORD>(dllBase + baseFunctionsAddress[baseOrdinalsAddress[i]]);

jak widać różnica w kodzie mała, ale w wyciągniętych danych ogromna.  Tablica ordinals zawiera po prostu wskazania kolejnych adresów funkcji.

Po udanym wykonaniu zadania postanowiłem opakować to w klasę dla łatwiejszego użytku. Co możemy dzięki temu osiągnąć? Przykładowo możemy uzywać funkcji bezpośrednio odwołując się do adresów zawartych w plikach .dll, które te funkcje eksportują bez includowania bibliotek.

Przykład:

#include <iostream>
#include "dlldumper.h"

using namespace std;

typedef void (*myprint)(...);
typedef double (*mypow)(double a, double b);

int main()
{
    DLLDumper dll;
    if(!dll.setDLL(L"msvcrt.dll",EAT))
        return 0;
    myprint print = (myprint)dll.getAddress("printf");
    mypow pow = (mypow)dll.getAddress("pow");
    if(print != NULL)
        print("Hello World %d\n",10);
    if(pow != NULL)
        cout << pow(2,3) << '\n';
    map<string,PDWORD>::iterator it;
    map<string,PDWORD>dllMap = dll.getMapedData();
    int i = 1;
    for(it = dllMap.begin();it!=dllMap.end();it++)
    {
        cout << i++ << ". " << it->first << " Address: " << it->second << '\n';
    }
    return 0;
}

Fragment listingu z wykonania kodu:
Hello World 10
8
...
480. _isalnum_l Address: 0x74a0aad5
481. _isalpha_l Address: 0x749cb4f3
482. _isatty Address: 0x749baf56
483. _iscntrl_l Address: 0x749d8c42
484. _isctype Address: 0x74a0ade5
485. _isctype_l Address: 0x74a0ae1b
486. _isdigit_l Address: 0x749c9dbd
487. _isgraph_l Address: 0x74a0abd3
488. _isleadbyte_l Address: 0x749bae3f
489. _islower_l Address: 0x749cb6a7
490. _ismbbalnum Address: 0x74a1a279
491. _ismbbalnum_l Address: 0x74a1a299
492. _ismbbalpha Address: 0x74a1a1fd
493. _ismbbalpha_l Address: 0x74a1a21d
494. _ismbbgraph Address: 0x74a1a2fb
...
Printf czy pow użyte bez includowania cstdio czy cmath ;) Dzięki tym danym będziemy mogli podmienić adres funkcji bezpośrednio w tablicy eksportów biblioteki na naszą funkcję i podsłuchiwać jakie dane są przesyłane. Wszystko jest też zależne od sposobu w jaki dany proces ładuje bibliotekę, czy bezpośrednio do własnego procesu, czy też wywołuje funkcje z biblioteki, a nie własne ich "kopie".

Ten problem da się rozwiązać przez podmianę adresów funkcji importowanych przez proces ;), dlatego rozwiązanie jest w miarę uniwersalne. 

2 komentarze:

  1. I mam rozumieć, że DLLDumper jest dostępny gdzie ?
    I jak wstawiasz listingi, że tak fajnie wyglądają ?

    OdpowiedzUsuń
  2. Kod można samemu sklecić na podstawie tutoriala. ;) Takie ćwiczenie. Na razie klasa nie nadaje sie do publikacji ze względu na małe błędy.

    OdpowiedzUsuń