Поиск KeServiceDescriptorTableShadow и восклицание об IsGUIThread

Discussion in 'Реверсинг' started by desTiny, 15 May 2009.

  1. desTiny

    desTiny Elder - Старейшина

    Joined:
    4 Feb 2007
    Messages:
    1,006
    Likes Received:
    444
    Reputations:
    94
    Диалог кода с Win XP:
    Код: Загрузи драйвер!
    XP: ОК!

    Диалог кода с Win Vista:
    Код: Загрузи драйвер!
    Vista: не буду (
    Код: Можно загрузить драйвер?
    Vista: ну, можно..
    Код: Загрузи драйвер!
    Vista: ОК!

    (из наблюдений)
    00:begin
    В предыдущем топике я что-то сказал об идее поиска KeServiceDescriptorTableShadow. И как-то код взял и написался...
    10:interface

    Напомню, в чём состояло предложение: ищем в тибе негуишного треда оффсет ServiceTable за счёт сравнения всей структурки с известной нам KeServiceDescriptorTableОбычнойСОченьДлиннымНазванием, потом переводим поток в гуи и читаем по найденному оффсету, где уже находится указатель на KeServiceDescriptorTableShadowСЕщёБолееДлиннымНазваниемЧемВыше​

    20:implementation
    От слов к делу. Начнём с драйвера. Он будет принимать IOCTL запросы и выводить на них оффсет ServiceTable либо адрес одной из интересующих нас таблиц.

    Для начала напишем код нахождения оффсета. А чтобы не сканировать многократно структурку, запишем его в структуру расширения устройства.
    Ниже я опускаю всякие проверки, но это всё есть в прилагаемом исходнике.
    Code:
            case IOCTL_FIND_OFFSET:
                //
                // Ищем офсет ServiceTable в ETHREAD
                //
          
                // тут проверка переданных параметров
                
                //указатель на буфер результата
                res = (ULONG *)Irp->AssociatedIrp.SystemBuffer; 
                
                //если ещё не искали оффсет, то там лежит -1, который записали в DriverEntry
                if (deviceExtension->ServiceTableOffset == -1)
                {
                    //основной код сканирования на предмет KeSDT
                    _asm
                    {
                        push esi
                        mov esi, fs:[0x124] //esi = ETHREAD
                        push eax
                        mov eax, KeServiceDescriptorTable 
                        push ecx 
                        mov ecx, 0x200 //должно хватить - максимальный офсет
                        
                        find: //цикл поиска KeServiceDescriptorTable в ETHREAD
                          cmp dword ptr [esi], eax 
                          jz found
                          dec ecx
                          jz not_found
                          inc esi
                        jmp find
                        found:
                          sub esi, fs:[0x124] //offset
                          mov offs, esi //переменная оффсета
                          jmp end
                        not_found:  
                          mov offs, -1
                        end:
                          pop ecx
                          pop eax
                          pop esi
                    }
                
                    //не нашли...
                    if ( offs == -1 )
                    {
                        *res = -1;
                        DebugPrint(("\t\tOffset ServiceTable not found\n"));
                        break;
                    }
    
                    deviceExtension->ServiceTableOffset = offs;
                } 
          
                //всё ок!
                *res = deviceExtension->ServiceTableOffset;
                DebugPrint(("\t\tOffset ServiceTable 0x%x\n", deviceExtension->ServiceTableOffset));
    
                //длина вывода
                OutBytes = 4;
    
                break;
    
    По коментариям должно быть ясно, что происходит.

    Теперь кусок кода, выводящий то, что записано по этому оффсету (взятого из DeviceExtension) :
    Code:
            case IOCTL_FIND_SSDT:
                //
                // Ищем KeServiceDescriptorTableShadow, зная офсет
                //
    
                //тут чекаем параметры
                
                //указатель на буффер результата
                res = (ULONG *)Irp->AssociatedIrp.SystemBuffer;
          
                //уже, вероятно, посчитанный офсет
                STOffset = deviceExtension->ServiceTableOffset;
          
                //а вдруг не посчитанный?
                if ( STOffset == -1 )
                {
                    status = STATUS_ACCESS_DENIED;
                    break;
                }
          
                //собственно, читаем
                _asm
                {
                     push eax
                     mov eax, fs:[0x124]
                     add eax, STOffset  //ETHREAD->ServiceTable
                     mov eax, [eax]     //надеемся, SSDT
                     push ecx
                     mov ecx, res
                     mov [ecx], eax     //записываем найденное SSDT в буфер
                     pop ecx
                     pop eax
                }
                
                //вернули 4 байта - дворд
                OutBytes = 4;
                break;
    
    Ну и так, для проверки - вывод KeSDT обычной
    Code:
    case IOCTL_GET_SDT: 
                //
                // Тут просто возвращаем KeServiceDescriptorTable
                //
    
                //чекаем параметры
                
                //указатель на буффер результата
                res = (ULONG *)Irp->AssociatedIrp.SystemBuffer;
          
                *res = (ULONG)KeServiceDescriptorTable;
    
                OutBytes = 4;
                break;
    
    На этом интересный код драйвера кончается. (На всякий случай, вышеприведённый код делался под спин-блокировкой).
    30: Usermode, странность и баг в виндовском коде
    Начнём со странности. Вроде я где-то читал, что поток в винде создаётся вначале как негуишный. Так вот - это не совсем правда ) Не знаю почему, но почему-то, на XP SP3 у меня стартовый поток загружается как негуишный, а на чужом XP SP3 почему-то уже при старте оказывается подгруженным User32.dll. Так что основной код будет выполняться под CreateThread'ом - он вроде везде нормально работает.


    ==> А теперь пара ласковых слов об IsGUIThread
    По msdn'у эта функция должна проверить поток на гуишность (как от неё ожидается), а если ей передан параметр TRUE, то попытаться сделать его таковым. Но почему-то код с этой функцией работал криво (на XP SP3)...И её асм листинг отвечает на вопрос "почему":
    Code:
      MOV EAX,DWORD PTR FS:[18] //указатель на TIB
      LEA ESI,DWORD PTR DS:[EAX+6CC] //LEA!!!
      NEG ESI
      SBB ESI,ESI
      NEG ESI //а вдруг esi == 0?
      JNZ SHORT label
      ...
      label: 
      mov eax, esi
      ret
    
    Но задаёт другой: "ну как так можно??" Это замечательный код проверяет, не указывает ли fs:[18] случайно на -0x6cc. А так редко бывает) Вывод - не юзайте эту функу.

    И ещё смешной момент - чтобы вызвать эту функцию, даже если бы она и работала, надо, чтобы к процессу была подгружена User32.dll, но которая в DllMain переводит вызывающий поток в гуй. Так что её (если бы она работала) имеет смысл использовать в одном потоке, только если она уже подгружена в другом.
    <== IsGUIThread

    Из-за сделанного замечания (а также из-за особенностей висты) main функция exe будет такой:
    Code:
        HANDLE hThread;
        //функция для того, чтобы дров грузился в висте - получаем привилегию грузить дрова (см. эпиграф)
        if (!SetLoadDriverPrivilege()){
            printf("Couldn't set privilege.. Trying without\n");
        }    
        //почему-то иногда стартовый поток запускается как GUI, а это плохо. Поэтому делаем отдельный поток.
        hThread = CreateThread( 
                NULL,                   // default security attributes
                0,                      // use default stack size  
                find,       // thread function name
                NULL,          // argument to thread function 
                0,                      // use default creation flags 
                NULL);   // returns the thread identifier
        
        //ждём, пока он кончится
        WaitForSingleObject(hThread, INFINITE);
        CloseHandle(hThread);
        _getch();
        return 0;
    
    Теперь немного о реализации переход в гуи. IsGUIThread не работает. User32.dll бывает уже подгруженной и её DllMain второй раз в потоке не сработает. Так что же делать? А мы так поступим - при вызове некоторых (большинства, а вероятно и просто всех) функций USER32.DLL поток переводится в гуи. Так что можно воспользоваться любой подходящей функцией... Мне понравилась GetDesktopWindow без параметров.

    Code:
    DWORD WINAPI find( LPVOID lpParam ) 
    /*
    
    Thread function
    
    */
    {
        HANDLE  hDevice;
        ULONG   ulReturnedLength;
        DWORD   errNum = 0;
        UCHAR   driverLocation[MAX_PATH];
        ULONG   offset;  //ServiceTable в ETHREAD
        BOOL    bStatus;
        ULONG   SDT_addr;
        ULONG   SSDT_addr;
        PVOID   user32func; //какая-нибудь функа для перхода в GUI
    
    /*
        это закомменчено из-за бажности апишки :(
    
        if (IsGUIThread(FALSE)){
          printf("Is GUI. Terminating..\n");
          return 1;
        } else {
          printf("Is not GUI. Well..\n");
        }
    */
    
        
        //
        // тут код, грузящий драйвер
        // и открывающий его в hDevice
    
    
        // получаем и записываем офсет ServiceTable из EPROCESS
        bStatus = DeviceIoControl(
            hDevice,                // Handle to device
            IOCTL_FIND_OFFSET,        // IO Control code
            NULL,              // Input Buffer to driver.
            0,        // Length of input buffer in bytes.
            &offset,                   // Output Buffer from driver.
            sizeof(offset),                      // Length of output buffer in bytes.
            &ulReturnedLength,      // Bytes placed in buffer.
            NULL                    // synchronous call
        );
    
        if ( !bStatus ) {
            printf("Ioctl failed with code %d\n", GetLastError() );
        } else {
            //именно -1 означало Not Found
            bStatus = offset != -1;
        }
        
        if ( !bStatus ) {
            printf("ServiceTable offset not found :(\n");
        } else {
            printf("Found ServiceTable offset: 0x%x\n", offset );
            
            /* bStatus = IsGUIThread(TRUE); - баг */
    
            //поэтому переводимся в GUI вызовом простенькой гуишной функи типа GetDesktopWindow
            //она без параметров
            user32func = GetProcAddress(LoadLibraryA("user32.dll"), "GetDesktopWindow");
            //чтобы не преобразовывать :)
            _asm call user32func;
    
            //надеюсь, тут багов не возникло :)
            bStatus = TRUE;
        }
    
        if ( !bStatus ) {
            //вообще это не выполнится...
            printf("Couldn't convert to GUI Thread: error code %d\n", GetLastError() );
        } else {
            //Для справки SDT
            bStatus = DeviceIoControl(
                hDevice,                // Handle to device
                IOCTL_GET_SDT,        // IO Control code
                NULL,              // Input Buffer to driver.
                0,        // Length of input buffer in bytes.
                &SDT_addr,                   // Output Buffer from driver.
                sizeof(SDT_addr),                      // Length of output buffer in bytes.
                &ulReturnedLength,      // Bytes placed in buffer.
                NULL                    // synchronous call
            );
        }
    
        if ( !bStatus ) {
            printf("Couldn't get normal SDT: error code %d\n", GetLastError() );
        } else {
            //выведем
            printf("KeServiceDescriptorTable: 0x%.8x\n", SDT_addr );
        
            //Кульминация - SSDT!
            bStatus = DeviceIoControl(
                hDevice,                // Handle to device
                IOCTL_FIND_SSDT,        // IO Control code
                NULL,              // Input Buffer to driver.
                0,        // Length of input buffer in bytes.
                &SSDT_addr,                   // Output Buffer from driver.
                sizeof(SSDT_addr),                      // Length of output buffer in bytes.
                &ulReturnedLength,      // Bytes placed in buffer.
                NULL                    // synchronous call
            );
        }
    
        if ( !bStatus ) {
            printf("Ioctl failed with code %d\n", GetLastError() );
        } else {
            printf("Found KeServiceDescriptorTableShadow: 0x%.8x\n", SSDT_addr );
        }
        
        CloseHandle(hDevice);
    
        //
        // Выгружаем драйвер
        //
    
        return 0;
    }
    
    Ну вот: основной код закончен, детали реализации - в прилагаемом исходнике.
    40: end
    Вот вроде и всё... Исходник готов, программа и драйвер скомпилены... И вроде работают)

    Вывод на XP SP3:
    Code:
    Found ServiceTable offset: 0xe0
    KeServiceDescriptorTable: 0x80562520
    Found KeServiceDescriptorTableShadow: 0x805624e0
    
    Правильно...

    //а от микрософта такой пакости с IsGUIThread не ожидал.

    Спасибо всем принявшим участие в тестировании кода!

    50: src + compiled

    (всё компилилось под WDK. Кстати, если кому интересно, загрузка драйвера реализована в двух вариантах - через SCM и ZwLoadDriver)
    Compiled: http://redxak.co.cc/ShadowSDT.rar
    Sources: http://redxak.co.cc/ShadowSDT_src.rar
     
    3 people like this.
  2. 0x0c0de

    0x0c0de Elder - Старейшина

    Joined:
    25 May 2007
    Messages:
    441
    Likes Received:
    396
    Reputations:
    297
    Code:
    case IOCTL_FIND_OFFSET:
    
    Слуушай, а зачем ты через ioctl запросы смещение ищещь? Почему бы сразу в DriverEntry не получить? )) И не надо этого всего с подгруженной/неподгруженной user32.dll, извращения какие-то. Проверила с kd сейчас на xpsp3, ок

    Ну и чисто эстетически вынеси в отдельную функцию. Какую-нибудь

    ULONG GetServiceTableOffset(VOID)

    например.
    Сам кодес не качала.

    PS 5*(+) )
     
  3. desTiny

    desTiny Elder - Старейшина

    Joined:
    4 Feb 2007
    Messages:
    1,006
    Likes Received:
    444
    Reputations:
    94
    >>Слуушай, а зачем ты через ioctl запросы смещение ищещь? Почему бы сразу в DriverEntry не получить? ))

    Можно и так... Я не знаю, какому из способов отдать предпочтение)


    >>И не надо этого всего с подгруженной/неподгруженной user32.dll, извращения какие-то.

    В смысле не надо? подгрузить её всё равно следует, а извращения в подгрузке библиотеки я не вижу)


    PS куда красивый INDENT дели? ))
     
  4. 0x0c0de

    0x0c0de Elder - Старейшина

    Joined:
    25 May 2007
    Messages:
    441
    Likes Received:
    396
    Reputations:
    297
    Я не про ту подгрузку. Я имела ввиду, что ты юзаешь CreateThread. И что в общем-то полаконичней будет сразу в DriverEntry получить нужный офсет.

    вообще не обязательно user32.dll. это может быть и gdi32.dll. важно просто вызвать сервис из win32k.sys. В той же gdi32 есть скажем вызовы какой-нибудь NtGdiDdUnlock. тогда в KiSystemService будет вызвана _PsConvertToGuiThread которая собственно и изменит указатель в KTHREAD.ServiceTable с KeServiceDescriptorTable на KeServiceDescriptorTableShadow
     
    #4 0x0c0de, 1 Jun 2009
    Last edited: 1 Jun 2009
  5. desTiny

    desTiny Elder - Старейшина

    Joined:
    4 Feb 2007
    Messages:
    1,006
    Likes Received:
    444
    Reputations:
    94
    А я и не просил user32)) я просто предложил) Понятно, что надо бы как-нибудь из ядра _PsConvertToGuiThread вызывать, чтобы в юзермоде не лезть, но я не хотел заморачиваться