Авторские статьи Анализ стека

Discussion in 'Статьи' started by _Great_, 3 Dec 2006.

  1. _Great_

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

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Article: Анализ стека
    Author: Great
    Date: 12.08.2006/3.12.2006
    Theme: Coding/Reversing
    Lang.: C/C++
    Base: -
    Note: Статья рассчитана знающих язык С++, программирующих на Win32 API и знающих основы вызова функций в ассемблере.

    I. Кручу-верчу, раскрутить хочу
    Вероятно, ты не раз видел во всяких полезных утилитах вроде отладчика или просмотрщика процессов одну полезную функцию - раскрутка стека. Если нет - поясню, что это такое. При вызове функции командой CALL в стеке сохраняется адрес возврата, который потом снимается оттуда командой RET. Проанализировав стек потока, можно узнать, каким путем и через какие вызовы выполнение добралось до текущей точки. Такая полезная возможность есть, например, в Process Explorer'е от небезызвестного Марка Руссиновича или, например, во встроенном отладчике в MS Visual Studio. Обычно, формат выводимой информации таков:
    имя_модуля!имя_функции + смещение [аргументы]
    Насчет аргументов. По идее, в стек запихиваются и переданные в функцию аргументы, поэтому можно при анализе стека еще и показывать аргументы. Правда, нигде не сохраняется информация об их размере, потому что вызываемая программа обычно знает размер своих аргументов. Но так как большая часть всех аргументов имеет размер 32 бита или 4 байта (это, например, int, long, enum, bool, все указатели и еще некоторые типы), это не большая проблема.)
    Попробуем и мы реализовать такую штуку, тем более, что это не сложно.

    II. Инструменты
    Для работы с именами функций и модулями мы будем использовать библиотеку DBGHELP.DLL, входящую в базовый комплект поставки детища дяди Билла. (По странной причине я не обнаружил в MS VC++ 6.0 к ней библиотеки импорта, хотя хидер там был - чудеса :). Пришлось спереть либу из masm32.) В ней для нас полезны следующие функции:
    SymInitialize: Подготовка процесса к работе с символами
    Code:
    BOOL SymInitialize(
      HANDLE hProcess,            // хендл процесса
      PSTR UserSearchPath,        // путь поиска. Ставим ноль в надежде, что все окажется в одном каталоге
      BOOL fInvadeProcess         // указан ли верный хендл процесса. Если FALSE, то вместо hProcess можно передать любое другое число, идентифицирующее процесс, например, его ID. Мы честно передаем хендл и ставим TRUE
    );
    
    Функция возвращает булево значение, означающее успешность выполнения.

    SymGetSymFromAddr: Возвращает имя функции и ее стартовый адрес по адресу какого-то, принадлежащего ей, байта.
    Code:
    BOOL SymGetSymFromAddr(
      HANDLE hProcess,             // хенлд процесса
      DWORD Address,               // адрес для исследования
      PDWORD Displacement,         // сюда запишут смещение адреса от стартового
      PIMAGEHLP_SYMBOL Symbol      // сюда запишут инфу о функции, в т.ч. ее адрес и имя
    );
    
    Формат структуры IMAGEHLP_SYMBOL таков:
    Code:
    typedef struct _IMAGEHLP_SYMBOL {
      DWORD SizeOfStruct;					 // размер структуры
      DWORD Address;							 // адрес
      DWORD Size;                  // размер
      DWORD Flags;                 // зарезервировано
      DWORD MaxNameLength;				 // макс. длина имени
      CHAR  Name[1];							 // имя
    } IMAGEHLP_SYMBOL, *PIMAGEHLP_SYMBOL;
    
    Т.к. длина имени может варьироваться, то выделять память под него должна вызывающая программа. Проще выделить память для всей структуры разом:
    Code:
    BYTE lpMemory[256];
    IMAGEHLP_SYMBOL* sym = (IMAGEHLP_SYMBOL*)lpMemory;
    
    Функция возвращает булево значение, означающее успешность выполнения.

    SymGetModuleBase: возвратить базовый адрес загрузки модуля по адресу какого-то его байта
    Code:
    DWORD SymGetModuleBase(
      HANDLE hProcess,  					 // хендл процесса
      DWORD dwAddr                 // адрес байта
    );
    
    Для инициализации DBGHELP.DLL понадобятся SymSetOptions().
    Code:
    DWORD SymSetOptions(
      DWORD SymOptions  
    );
    
    Опции могут быть следующими:
    SYMOPT_CASE_INSENSITIVE Поиск имен ведется без учета регистра символов
    SYMOPT_UNDNAME Все имена представлены в обычном виде (без постфиксов @N, характерных для соглашения вызова stdcall)
    SYMOPT_DEFERRED_LOADS Имена не загружаются, пока не возникнет ссылка на них. Это наиболее быстрый способ использования имен.
    SYMOPT_NO_CPP Во всех именах C++, содержащих '::', будет произведена замена на '__'
    SYMOPT_LOAD_LINES Загрузить информацию о номерах строк


    Так же нам понадобится стандартная API GetModuleFileName()
    III. Кодинг
    Первое, что мы напишем, будет функция получения имени функции и ее стартового адреса - оболочка для SymGetSymFromAddr.
    Сначала надо проинициализировать процесс. Мы вызовем SymSetOptions и укажем DBGHELP.DLL производить отложенную загрузку и раздекорировать имена.
    Далее мы выделим память для структуры IMAGEHLP_SYMBOL и вызовем SymGetSymFromAddr.
    Итак,
    Code:
    /* GetProcBaseAddressAndName()
     *
     * Описание
     *		Функция возвращает стартовый адрес функции и ее имя по переданному адресу (адрес может и не указывать на
     *		начало функции).
     *
     * Параметры
     *	hProcess	хендл процесса
     *	dwAddress	адрес
     *	lpdwBase	адрес переменной, куда будет записан стартовый адрес
     *	lpszProcName строка, куда будет записано имя функции
     *	nSize		длина этой строки
     *
     * Возвращаемое значение
     *  TRUE, если поиск удался, в противном случае - FALSE
     */
    BOOL GetProcBaseAddressAndName(HANDLE hProcess, DWORD dwAddress, LPDWORD lpdwBase, LPDWORD lpdwOffset, LPTSTR lpszProcName, DWORD nSize)
    {
    	// Иницаализация процесса 
    	SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
    	if (!SymInitialize(hProcess, NULL, TRUE))
    		return 0;
    
    	// Ищем функцию
    	BYTE      buffer[256];
    	PIMAGEHLP_SYMBOL pSymbol = (PIMAGEHLP_SYMBOL)buffer;
    
    	pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL);
    	pSymbol->MaxNameLength = sizeof(buffer) - sizeof(IMAGEHLP_SYMBOL) + 1;
    
    	if (!SymGetSymFromAddr(hProcess, dwAddress, lpdwOffset, pSymbol))
    		return 0;
    
    	strncpy(lpszProcName, pSymbol->Name, nSize);
    	*lpdwBase = pSymbol->Address;
    	return 1;
    }
    
    Далее напишем функцию, которая нам по заданному адресу возвратит строку вида "модуль!функция+смещение", где "модуль" - имя модуля, "функция" - найденная функция, "смещение" - смещение заданного адреса относительно ее базового адреса. Если адрес не найдем, возвратим "модуль!адрес", если не найдем модуль - возвратим "unknown!адрес".
    Code:
    /* GetModuleAndFunctionNameByAddress()
     *
     * Описание
     *		Функция возвращает строку вида "имя_модуля!имя_функции + смещение" по переданному адресу. К примеру, если передать
     *		ExitProcess+0x1a, функция вернет строку "kernel32!ExitProcess + 0x001a". Если функция не может найти адрес,
     *		строка будет вида "имя_модуля!адрес", а если не может найти модуль - имя модуля будет "unknown"
     *
     * Параметры
     *	hProcess	хендл процесса
     *	dwAddress	адрес
     *	lpszString	строка
     *	nSize		длина строки
     *
     * Возвращаемое значение
     *  TRUE, если поиск удался, в противном случае - FALSE
     */
    BOOL GetModuleAndFunctionNameByAddress(HANDLE hProcess, DWORD dwAddress, LPTSTR lpszString, DWORD nSize)
    {
    	DWORD base,offset;
    	char name[1024], ret[10240];
    
    	if(!GetProcBaseAddressAndName(hProcess, dwAddress, &base, &offset, name, sizeof(name)))
    	{
    		if(GetLastError()==ERROR_MOD_NOT_FOUND || GetLastError()==ERROR_INVALID_ADDRESS)
    		{
    			if(!dwAddress)
    			{
    				wsprintf(ret, "unknown!0x%08x", dwAddress);
    				strncpy(lpszString, ret, nSize);
    				return 1;
    			}
    
    			DWORD module = SymGetModuleBase(hProcess, dwAddress);
    			if(!module)
    			{
    				wsprintf(ret, "unknown!0x%08x", dwAddress);
    				strncpy(lpszString, ret, nSize);
    				return 1;
    			}
    			
    			IMAGEHLP_MODULE modinfo = {sizeof(modinfo)};
    			if(!SymGetModuleInfo(hProcess, module, &modinfo))
    			{
    				wsprintf(ret, "unknown!0x%08x", dwAddress);
    				strncpy(lpszString, ret, nSize);
    				return 1;
    			}
    
    			wsprintf(ret, "%s!0x%08x", modinfo.ModuleName, dwAddress);
    			strncpy(lpszString, ret, nSize);
    			return 1;
    		}
    		return 0;
    	}
    
    	DWORD module = SymGetModuleBase(hProcess, dwAddress);
    	IMAGEHLP_MODULE modinfo = {sizeof(modinfo)};
    	SymGetModuleInfo(hProcess, module, &modinfo);
    	
    	wsprintf(ret, "%s!%s + 0x%04x", modinfo.ModuleName, name, offset);
    
    	strncpy(lpszString, ret, nSize);
    	return 1;
    }
    
    Теперь все готово, для того, чтобы проанализировать стек потока и посмотреть, какие функции были вызваны и каким путем выполнение программы пришло в текущую точку. Например, мы хотим проанализировать последние 20 адресов в стеке. Мы прочитаем 20 чисел по адресу EBP текущего потока и проанализируем их.
    Code:
    /* StackUnwind()
     *
     * Описание
     *		Раскрутка локального стека по адресу из EBP. Отображает окно с информацией
     *
     * Параметры
     *	nSize		глубина раскрутки. Столько адресов, начиная с вершины стека, будет проанализировано
     *	bSkipUnknownModules	флаг - пропускать "бесхозные" адреса (имена модулей которых получить не удалось)
     *
     * Возвращаемое значение
     *  нет
     */
    void StackUnwind(DWORD nSize=10, BOOL bSkipUnknownModules = TRUE)
    {
    	DWORD *__ebp;
    	_asm mov __ebp, ebp; // сохраним значение EBP
    
    	char buffer[10240] = ""; // сюда запишем всю информацию для вывода через MessageBox
    
    	wsprintf(buffer+lstrlen(buffer),"Unwinding stack by EBP=0x%08x\n", __ebp);
    
    	char buf[1024];
    	for(DWORD i=0;i<nSize;i++)
    	{
    	  // получаем имя функции в виде "модуль!функция+смещение"
    		GetModuleAndFunctionNameByAddress(GetCurrentProcess(), (DWORD)__ebp[i], buf, 1024);
    		// имя модуля определить не удалось => это не адрес возврата, а, скорее всего аргумент функции.
    		if(!strncmp(buf, "unknown", 7) && bSkipUnknownModules)
    			continue;
    		wsprintf(buffer+lstrlen(buffer), "0x%08x %s\n", __ebp[i], buf);
    		if(!strcmp(buf, "kernel32!RegisterWaitForInputIdle + 0x0049"))
    		{
    			wsprintf(buffer+lstrlen(buffer), "Found kernel32!BaseProcessStart (shown as 'kernel32!RegisterWaitForInputIdle + 0x0049'), stopping\n");
    			break;
    		}
    	}
    
    	MessageBox(0, buffer, "Stack unwind information", MB_ICONINFORMATION);
    }
    
    В цикле видно странный if, в котором проверяется совпадение имени функции с kernel32!RegisterWaitForInputIdle + 0x0049. Зачем это нужно, мы узнаем чуть позже.

    ...
     
    #1 _Great_, 3 Dec 2006
    Last edited: 3 Dec 2006
    7 people like this.
  2. _Great_

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

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Теперь мы можем вызвать эту функцию прямо из WinMain и узнать, что творится в стеке главного потока нашей программы. Но мы для наглядности напишем пару-тройку вызывающих друг друга процедур, чтобы проследить местоположение адресов возврата в стеке.
    Code:
    void f3()
    {
    	StackUnwind(50);
    }
    
    void f2()
    {
    	f3();
    }
    
    void f1()
    {
    	f2();
    }
    
    Мы решили проанализировать 50 адресов в стеке. Из WinMain мы вызываем функцию f1, она вызывает f2, f2, в свою очередь, вызывает f3, которая вызывает StackUnwind. Что ж, поставим точку входа в нашу программу на WinMain, чтобы нам не мешали всякие WinMainCRTStartup, и запустим ее.
    Результат анализа стека показан ниже:
    По первым 3-4 строчкам видно, что WinMain вызвала f1, и по цепочке управление дошло до функции f3.
    Но что за странные строчки после WinMain и до конца?
    А все дело в том, что выполнение любого потока (кроме главного потока программы) начинается с функции BaseThreadStart, а главного потока программы - с BaseProcessStart. Она устанавливает некоторые параметры потоку, в том числе и истинную точку входа и передает управление на неё - в данном случае, на WinMain. Почему же не распозналась функция BaseProcessStart, а вместо нее идет RegisterWaitForInputIdle, которая, казалось бы, совсем не к месту? А все дело в причудливом расположении этих функций в kernel32.dll.
    Сначала идет BaseProcessStart, в которой есть переход на 64С0 байт вперед, за ее пределы, потом идет RegisterWaitForInputIdle, а потом идет продолжение BaseProcessStart, куда она перепрыгивала с 64С0 байт назад.
    Выглядит все это примерно так (листинг IDA Pro):
    Code:
    .text:7C810867 BaseProcessStart proc near              ; DATA XREF: sub_7C81059D+47D4o
    .text:7C810867                 xor     ebp, ebp
    .text:7C810869                 push    eax
    .text:7C81086A                 push    0
    .text:7C81086C                 jmp     __BaseProcessStartContinue
    .text:7C81086C BaseProcessStart endp
    
    ... тут идет множество других функций, в том числе и RegisterWaitForInputIdle, которая идет последней ...
    
    .text:7C816D06 ; int __fastcall RegisterWaitForInputIdle(int,int,int)
    .text:7C816D06                 public RegisterWaitForInputIdle
    
    а сюда управление попадает из BaseProcessStart. С легкой руки эту метку я обозвал __BaseProcessStartContinue
    
    .text:7C816D2C ; START OF FUNCTION CHUNK FOR BaseProcessStart
    .text:7C816D2C
    .text:7C816D2C __BaseProcessStartContinue:             ; CODE XREF: BaseProcessStart+5j
    .text:7C816D2C                 push    0Ch
    .text:7C816D2E                 push    offset dword_7C816D58
    .text:7C816D33                 call    sub_7C8024CB
    .text:7C816D38                 and     dword ptr [ebp-4], 0
    .text:7C816D3C                 push    4
    .text:7C816D3E                 lea     eax, [ebp+8]
    .text:7C816D41                 push    eax
    .text:7C816D42                 push    9
    .text:7C816D44                 push    0FFFFFFFEh
    .text:7C816D46                 call    ds:NtSetInformationThread
    .text:7C816D4C                 call    dword ptr [ebp+8]
    .text:7C816D4F                 push    eax             ; dwExitCode
    .text:7C816D50
    .text:7C816D50 loc_7C816D50:                           ; CODE XREF: .text:7C843635j
    .text:7C816D50                 call    ExitThread
    
    Сначала управление попало в BaseProcessStart, процессор перепрыгнул на __BaseProcessStartContinue и произошел вызов NtSetInformationThread.
    Смещение 0x0046 относительно RegisterWaitForInputIdle - это адрес инструкции
    Code:
    .text:7C816D4C                 call    dword ptr [ebp+8]
    а смещение 0x0049 - адрес
    Code:
    .text:7C816D4F                 push    eax             ; dwExitCode
    И все тайное становится явным :)
    Адрес возврата в ntdll!NtSetInformationThread появляется просто по причине того, что в ntdll содержатся функции-переходники для Native API.
    Как раз NtSetInformationThread является одним из таких переходников. Смещение 0x000c относительно NtSetInformationThread - это адрес инструкции после call, затолкнутый процессором в стек при выполнении инструкции call.
    Code:
    .text:7C90E642                 public ZwSetInformationThread
    .text:7C90E642 ZwSetInformationThread proc near        ; CODE XREF: RtlImpersonateSelf+71p
    .text:7C90E642                                         ; sub_7C92764E+79DCp ...
    .text:7C90E642                 mov     eax, 0E5h       ; NtSetInformationThread
    .text:7C90E647                 mov     edx, 7FFE0300h
    .text:7C90E64C                 call    dword ptr [edx]
    .text:7C90E64E                 retn    10h
    .text:7C90E64E ZwSetInformationThread endp
    Кроме того, что мы рассмотрели, как анализировать стек текущего процесса, мы еще и узнали, как происходит старт нового процесса.
    Попробуем теперь распотрошить стек любого другого процесса.
    Что же нам потребуется - найдем первый попавшийся поток этого процесса, приостановим, снимем его контекст, снимем дамп его стека и возобновим поток.
    Потом будем производить анализ стека точно так же, как и для текущего процесса.

    Поскольку я использую Microsoft Visual C++ 6.0 и ленюсь обновить свой давно устаревший SDK :), а функция OpenThread отсутствовала в ранних версиях Windows, мне пришлось написать оболочку для динамического вызова OpenThread:
    Code:
    HANDLE WINAPI OpenThread(
      DWORD dwDesiredAccess,  // access right
      BOOL bInheritHandle,    // handle inheritance option
      DWORD dwThreadId        // thread identifier
    )
    {
    	typedef HANDLE (WINAPI *func)(DWORD,BOOL,DWORD);
    	func f = (func)GetProcAddress(GetModuleHandle("kernel32.dll"), "OpenThread");
    	if(!f)
    		ExitProcess(MessageBox(0, "Cannot find 'OpenThread' entry point in the 'kernel32.dll'", 0, MB_ICONERROR));
    	return f(dwDesiredAccess, bInheritHandle, dwThreadId);
    }
    
    Теперь напишем функцию для анализа стека процесса. Ее заголовок будет таким:
    BOOL StackRemoteUnwind(DWORD dwProcessId, DWORD dwThreadId, DWORD nSize=10, BOOL bSkipUnknownModules = TRUE)
    dwProcessId - ID процесса
    dwThreadId - ID потока
    nSize - сколько адресов анализировать
    bSkipUnknownModules - пропускать ли адреса, чьи модули не известны.

    Для начала открываем дескрипторы процесса и потока:
    Code:
    	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwProcessId);
    	if(!hProcess)
    		return MessageBox(0,"Cannot open process", "Stack unwind", MB_ICONSTOP)?0:0;
    
    	HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, 0, dwThreadId);
    	if(!hThread)
    		return MessageBox(0,"Cannot open thread", "Stack unwind", MB_ICONSTOP)?0:0;
    
    Затем аккуратно останавливаем поток, снимаем всю инфу, которая нужна (контекст, дамп стека) и возобновляем выполнение потока
    Code:
    	CONTEXT ctx = {CONTEXT_FULL};
    	SuspendThread(hThread); // приостанавливаем
    	if(!GetThreadContext(hThread, &ctx) || !ctx.Eip) // получаем контекст
    	{
    		ResumeThread(hThread);
    		return MessageBox(0, "Cannot get thread context", "Stack unwind", MB_ICONSTOP)?0:0;
    	}
    	DWORD stackptr = ctx.Esp; // снимаем стек по адресу в ESP того потока
    	char stack[10240];
    	DWORD* lpdwStack = (DWORD*)stack; // делаем указатель на массив DWORD'ов для удобного анализа адресов
    	DWORD read=0;
    	if(!ReadProcessMemory(hProcess, (LPVOID)stackptr, stack, nSize*4, &read)) // снимаем дапм стека
    	{
    		ResumeThread(hThread);
    		return MessageBox(0, "Cannot read process memory", "Stack unwind", MB_ICONSTOP)?0:0;
    	}
    	ResumeThread(hThread); // не забываем возобновить поток :)
    
    Далее производим точно такие же действия, что и для текущего процесса:
    Code:
    	char buffer[10240] = "";
    	wsprintf(buffer+lstrlen(buffer),"Unwinding stack at address 0x%08x\n", stackptr);
    
    	char buf[1024];
    	for(DWORD i=0;i<nSize;i++)
    	{
    		GetModuleAndFunctionNameByAddress(hProcess, lpdwStack[i], buf, 1024);
    		if(!strncmp(buf, "unknown", 7) && bSkipUnknownModules)
    			continue;
    		wsprintf(buffer+lstrlen(buffer), "0x%08x %s\n", lpdwStack[i], buf);
    	}
    
    	MessageBox(0, buffer, "Stack unwind information", MB_ICONINFORMATION);
    
      // не забываем закрыть дескрипторы потока и процесса
    	CloseHandle(hThread);
    	CloseHandle(hProcess);
    	return 1;
    
    Для нахождения процесса по имени его образа не помешала бы функция, выполняющая эту операцию:
    Code:
    /* GetPIDbyName()
     *
     * Описание
     *		Получает идентификатор процесса по его имени
     *
     * Параметры
     *	szProcessName		имя процесса
     *
     * Возвращаемое значение
     *  PID процесса
     */
    DWORD GetPIDbyName(LPTSTR szProcessName)
    {
    	HANDLE hSnapshot;
    	PROCESSENTRY32 pe = {sizeof(pe)};
    
    	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    	if (hSnapshot == INVALID_HANDLE_VALUE)
    		return 0; 
    
    	if (!Process32First(hSnapshot, &pe))
    		return 0;
    
    	do
    		if(!lstrcmpi(pe.szExeFile,szProcessName)) 
    			return pe.th32ProcessID;
    	while (Process32Next(hSnapshot, &pe)); 
    	return 0;
    }
    
    Теперь все готово для анализа стека потока другого процесса.
    Code:
    	// Создаем снимок всех потоков системы
    	HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    	if(hSnapShot == INVALID_HANDLE_VALUE)
    		return MessageBox(0, "CreateToolhelp32Snapshot() failed", "Thread snapshot", MB_ICONERROR)?0:0;
    
    	THREADENTRY32 te = {sizeof(te)};
    	if(!Thread32First(hSnapShot, &te))
    		return AlternateMessageBox("No threads", "Thread snapshot")?0:0;
    
    	DWORD pid=GetPIDbyName("explorer.exe");
    	// Крутим список всех потоков
    	do
    	{
    		// Раскручиваем стек первому попавшемуся потоку нашего процесса
    		if(te.th32OwnerProcessID == pid)
    		{
    			StackRemoteUnwind(te.th32OwnerProcessID, te.th32ThreadID, 200, TRUE);
    			break;
    		}
    	}
    	while(Thread32Next(hSnapShot, &te));
    
    Приведенный код находит первый поток процесса explorer.exe и анализирует его стек.
    Вообще то, на этом стоит и закончить и без того затянувшееся повествование об устройстве потоков :)
    Замечу только, что в сорце к статье вместо MessageBox для вывода информации используется самописная функция AlternateMessageBox, которая выводит диалоговое окно с текстовым полем для удобства копирования текста из него. Выход производится по нажатию любой клавиши. Код этой функции, если сильно повезет, ты найдешь в сорсе к статье, и я думаю, что знакомые с программированием окон на Win32 API, без труда его разберут.
    На сим откланяюсь, удачного компилирования :)

    (С) Great, 2006.

    Source: http://cribble.by.ru/data/stackunwind.cpp
     
    #2 _Great_, 3 Dec 2006
    Last edited: 3 Dec 2006
    2 people like this.