Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Delphi: WinAPI и системное программирование > Перехват API


Автор: <Spawn> 30.8.2003, 14:41
Сделал перехват WinSock API функции recv, но вот теперь встал вопрос каким образом лучше пытаться анализироавть приходящие пакеты(ведь в приходящем буфере сплошной массив байтов. Возможно, что первый байт(слово, двойное слово) это идентификатор сообщения, а вот остальное...)? Есть ли какие нибудь идеи на этот счет? Сразу говорю, что это не троянописательство. Пытаюсь помочь одному человеку, администрирующему один из игровых серверов "Пурги", но для защиты от читеров необходимо вести некоторые уточняющие логи(возможно с автобаном), чего в самом сервере не предусмотрено. Заранее спасибо.

Автор: Baa 30.8.2003, 21:25
<Spawn>, а зачем было перехватывать функцию, если можно перехватывать пакеты? А анализировать их на предмет чего надо?
з.ы. а так и должно быть, что это непрерывный массив байт.

Автор: stab 30.8.2003, 21:30
<Spawn> похоже тебе придется разбираться с сетевым протоколом этой игры...

Автор: <Spawn> 30.8.2003, 22:44
Baa Перехватывать нужно различные незаконные действия пользователя, будь то дюп предметов, или Плайер Киллерство, на не разоешенных для этого картах.

Автор: stab 31.8.2003, 12:17
ага, все таки придется разбираться smile.gif а что за игра?

Автор: Baa 31.8.2003, 13:38
Игра на УО похожа smile.gif
Тут придется делать так: ловишь пакеты, сам делаешь незаконное действие и пытаешься анализировать пакет. Смори данные , которые не изменяются (скорее всего карта, правда могут быть и координаты, тогда это хуже, но тоже решимо. А киллерство думаю, что отследить можно, ведь при ударе посылается пакет с ударом, смори обычные пакеты, потом бей и смори изменения, главное совершать поменьше телодвижений, дабы данных меньше менялось... а если у игрушки шифрование пакетов.... то плакала твоя затея wink.gif

Автор: <Spawn> 31.8.2003, 15:39
Нее, не УО - сервак Аллодов 2))) Если бы это УО было бы, то было б легче. До меня уже поработали - все пакеты известны. Тем более в УО (при использовании POL-а) проще все подобное делать УО-шными скриптами.

Автор: Baa 2.9.2003, 14:00
Могу предложить дизассемблировать сервак (или оттрейсить) и посмореть, как он сам пакеты разбирает.

Автор: Akella 2.9.2003, 20:06
Я нашел такую статейку:

--------Использование Debug API: пример перехвата вызовов функций Win32 API
С ЧЕГО ВСЕ НАЧИНАЛОСЬ:
С начала. Мне нужно было написать перехватчик вызовов WinSock. Дабы любая программа могла работать через SOCKS5-проксик. Я посчитал, что перехват вызовов DLL'ки проще, чем судорожные попытки написать драйвер (да и сейчас так считаю). Енота, правда, ехидно улыбалась и говорила "ну-ну", но я-таки справился. SOCKS сниффер еще пишу, но в принципах перехвата уже разобрался :-) [Енота: разобраться-то он действительно разобрался, а соксифиера нет до сих пор...]
КАК ВСЕ БУДЕТ:
Я предпочитаю не писать сухие статьи с кучей теории. Поскольку я люблю читать работающий исходный код, то и здесь будет только исходный код. Все пояснения я буду вставлять прямо в исходник - в виде комментариев. Впрочем, не надейтесь, что вам будет достаточно выдрать отсюда исходник, и он скомпилится. :-) Это не потому, что я специально что-то скрыл, а потому, что я вырезал кучу вспомогательных процедур, которые каждый может написать сам. Если вы, все же, паталогически ленивы - скачайте архив с полными рабочими исходниками. Оттуда точно заработает.
ИСХОДНИКИ:
Наконец-то... начнем.
Код
procedure DoDebugLoop;
{ собственно, это главная процедура перехватчика. большую часть времени он крутится именно в ней }
var
Event: TDebugEvent;
{ стандартная Win32 стректура. для интересующихся:
ЕDebugEvent = record
dwDebugEventCode: DWORD;
// тип пришедшего события
dwProcessId: DWORD;
// Id прерванного процесса
dwThreadId: DWORD;
// Id прерванного потока
case Integer of
0: (Exception: TExceptionDebugInfo);
1: (CreateThread: TCreateThreadDebugInfo);
2: (CreateProcessInfo: TCreateProcessDebugInfo);
3: (ExitThread: TExitThreadDebugInfo);
4: (ExitProcess: TExitThreadDebugInfo);
5: (LoadDll: TLoadDLLDebugInfo);
6: (UnloadDll: TUnloadDLLDebugInfo);
7: (DebugString: TOutputDebugStringInfo);
8: (RipInfo: TRIPInfo);
// эти части смотрите сами - не могу же я все разжевывать! :-)
end;
следует добавить, что Microsoft - ребята странные. Функция GetThreadContext, при помощи которой реализуется пошаговая отладка и просмотр регистров, требует на входе хэндл процесса. а нам дают только его Id. после безуспешных поисков функции типа ConvertThreadIdToHandle [Енота: мечтатель, однако...] я решил, что придется заводить список запущенных потоков. в событии CREATE_THREAD_DEBUG_EVENT нам дают-таки хэндл. придется запоминать все созданные потоки (не забывая их забывать ( сорри :-) в EXIT_THREAD_DEBUG_EVENT). позже Sleepyhead сказал, что я все придумал очень правильно (ай да Кэтмар! ай да [censored 7] сын! простите, классика :-) - так люди и делают. ну он большой, ему виднее :-) }
dwContinueStatus: DWORD;
{ как системе обрабатывать событие в ContinueDebugEvent. обнаружилось, что если это событие - не исключение (EXCEPTION_DEBUG_EVENT), то этот флажок системе "по сараю". а если исключение, то есть два варианта: DBG_CONTINUE - наш "отладчик" успешно обработал все сам, и DBG_EXCEPTION_NOT_HANDLED, что значиит - передать исключение системе на обработку } 
CurThread: DWORD;
{ хэндл потока, найденный в нашем списке потоков (см. замечание чуть повыше) }
HProc: DWORD;
{ хэндл процесса, который мы отлаживаем }
Context: TContext;
{ контекст потока. проще говоря - содержание его регистров }
ThreadList: array[0..99] of record Id, Handle: DWORD; end;
{ тот самый пресловутый список потоков, который мы своими ручками будем создавать и поддерживать. в принципе, это должен быть список или динамический массив, ибо количество потоков, которые может создать программа, заранее не известно, но не будем заморачиваться. код-то демонстрационный! }
RetAddr: DWORD;
{ здесь будет храниться адрес возврата из перехваченной API-функции (так, на всякий случай. чтобы вы видели, как и откуда его можно добыть) }
BPAddr: DWORD;
{ в учебных целях мы будем перехватывать только одну функцию. поэтому вместо списка обойдемся просто переменной. здесь будет храниться адрес первого байтика перехваченной функции }
OrigByte: Byte;
{ а здесь будет храниться сам первый байтик }
RestoreBreak: Boolean;
{ флажок, который указывает обработчику события EXCEPTION_SINGLE_STEP надо ли восстанавливать точку останова. весь перехват выглядит так:
нашли стартовый адрес процедуры (это можно сделать просмотром таблицы экспорта у соответствующей DLL-ки. как именно - здесь не пишу. или разбирайтесь сами, или качайте мои исходники - там все есть. не то чтобы мне жалко, но к Debug API это имеет отношение весьма косвенное. опять же, если народ будет очень интересоваться, сделаю статью с quick overview формата PE);
запомнили ее первый байт;
записали вместо первого байта код $CC (это Int3 - DEBUG_EXCEPTION);
по приходу DEBUG_EXCEPTION:
проверили, точно ли мы прервались на адресе нашей точки останова. если нет - не делаем ничего. иначе:
восстановили первый байт;
установили флажок SINGLE_STEP;
установили флажок ResoteBreak;
ожидаем прихода события EXCEPTION_SINGLE_STEP;
по приходу EXCEPTION_SINGLE_STEP:
если установлен флажок RestoreBreak:
вернули на место $CC;
сбросили флажок ResoteBreak; }
ProcessFinished: Boolean;
{ флажок, указывающий, завершился ли отлаживаемый процесс. Sleepyhead говорит, что иногда процесс не завершается корректно (к примеру, отладчик, который отлаживает отладчик, который отлаживает отладчик... [Енота: GNU's not Unix :-)]), поэтому если процесс не завершится сам, мы прибьем его руками }
begin
FillChar(ThreadList, SizeOf(ThreadList), 0);
HProc := 0;
{ хэндл процесса, который будем отлаживать. пока процесс не запущенным считается, соответственно - хэндла нету }
ProcessFinished := True;
{ поскольку процесс не запустился, то он считается завершенным :-) }
BPAddr := 0;
{ точку останова уточним, когда загрузится нужная DLL }
RestoreBreak := False;
repeat
if not WaitForDebugEvent(Event, INFINITE) then break;
{ ожидаем прихода отладочного события. в реальном отладчике здеесь вместо INFINITE лучше задать маленькую константу, ожидать в цикле, там же в цикле организовывать взаимодействие с юзверем. или вообще для интерфейса отдельный поток создать }
dwContinueStatus := DBG_EXCEPTION_NOT_HANDLED;
{ поскольку большинство исключений мы не обрабатываем, то по умолчанию так и говорим системе }
CurThread := GetThreadHandleFromList(ThreadList, Event.dwThreadId);
{ просто поиск в массиве ThreadList. Id нам известен, ищем хэндл }
case Event.dwDebugEventCode of
{ проверим - а что, собственно случилось? }
CREATE_PROCESS_DEBUG_EVENT:
{ запустился новый процесс. запомним его хэндл, и сбросим флажок ProcessFinished }
begin
HProc := Event.CreateProcessInfo.HProcess;
ProcessFinished := False;
AddThreadToList(ThreadList, Event.dwThreadId, Event.CreateProcessInfo.hThread);
end;
EXIT_PROCESS_DEBUG_EVENT:
{ процесс завершился - значит, можно смело закрывать наш перехватчик. заодно установим флажок ProcessFinished }
begin
ProcessFinished := True;
ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId, DBG_CONTINUE);
{ это на всякий случай - чтобы ось точно прибила и процесс, и отладчик. в принципе, оно не надо, но смотри выше комментарий к ProcessFinished }
break; { все, из цикла отладки можно смело выходить }
end;
CREATE_THREAD_DEBUG_EVENT: 
{ процесс запустил новый поток. здесь у нас есть единственная возможность запомнить его хэндл. так и делаем }
AddThreadToList(ThreadList, Event.dwThreadId, Event.CreateThread.hThread);
EXIT_THREAD_DEBUG_EVENT:
{ процесс завершил исполнение потока. забудем его хэндл }
DeleteThreadFromList(ThreadList, Event.dwThreadId);
LOAD_DLL_DEBUG_EVENT:
{ процесс загрузил какую-то DLL'ку. проверим, не та ли это, которая нам нужна. если та, установим точку останова. текст процедуры смотрите ниже }
ProcessDLLExport(HProc, DWORD(Event.LoadDll.lpBaseOfDll));
UNLOAD_DLL_DEBUG_EVENT:
{ процесс выгрузил какую-то DLL'ку. по-правилам, это надо бы обработать, но поскольку я перехватываю вызовы kernel32.dll, который всегда (за очень-очень редким исключением :-) линкуется статически, то это событие я просто игнорирую. а вообще-то надо запомнить адрес загрузки нужной нам DLL в LOAD_DLL_DEBUG_EVENT (ибо это единственный способ идентифицировать DLL'ку), а здесь проверять - не наша ли это. если наша - обнулить BPAddr. можете дописать сами - как любят говорить авторы книг: "в качестве упражнения" :-) [Енота: ага. а сам, когда видит в книге эту фразу, разражается потоком нецензурной лексики :-)] }
WriteLn('unloading DLL: ', IntToHex(DWORD(Event.UnloadDll.lpBaseOfDll), 8));
EXCEPTION_DEBUG_EVENT:
{ какое-то исключение. проверим поточнее... }
case Event.Exception.ExceptionRecord.ExceptionCode of
EXCEPTION_BREAKPOINT:
{ это - точка останова. здесь мы уточним: наша или нет. дело в том, что система сама генерирует это событие, когда процесс загрузился, но перед тем, как он запущен (полсе того, как системный загрузчик загрузил процесс и все его DLL'ки. как раз перед тем, как исполнить первую инструкцию процесса). плюс - мало ли, какой код внутри исследуемого процесса может быть? так что... }
begin
dwContinueStatus := DBG_CONTINUE;
{ скажем системе, что это исключение мы обработали сами, пусть не напрягается }
Context.ContextFlags := CONTEXT_CONTROL or CONTEXT_INTEGER or CONTEXT_SEGMENTS;
GetThreadContext(CurThread, Context);
{ получили контекст прерванного потока. больше всего нас интересуют IP и Flags. остальные регистры запросили просто для полноты картины }
if (BPAddr <> 0) and (Context.EIP = BPAddr + 1) then
begin
{ если мы уже установили нашу точку останова и прервались именно на ней... }
RetAddr := ReadProcessLong(HProc, Context.ESP);
{ то получим адрес возврата из перехваченной нами функции. он нам не нужен, на самом-то деле, это просто пример - откуда его брать. если вам нужны параметры - ReadProcessLong(HProc, Context.ESP + 4) будет первым, ...+ 8) - вторым, и так далее... кстати, ReadProcessLong - просто обертка для системной функции ReadProcessMemory. читает 4 байтика. для удобства. думаю, что у вас не будет проблем сделать себе такую же :-) }
WriteLn('Return address: 0x', IntToHex(RetAddr, 8));
{ дальше - уменьшим IP на еденичку (чтобы исполнить ту инструкцию, которую мы заменили на нашу точку останова)... реально, EIP-1 хранится в BPAddr. так и запишем... }
Context.EIP := BPAddr;
{ ...и восстановим оригинальный первый байтик этой инструкции }
WriteProcessByte(HProc, BPAddr, OrigByte);
{ установим флажок для того, чтобы система генерировала событие EXCEPTION_SINGLE_STEP. в этом событии надо будет вернуть точку останова на место, иначе перехват состоится ровно один раз :-) [Енота: а то бы читатель сам не догадался...] }
RestoreBreak := True;
Context.EFlags := Context.EFlags or EFLAGS_TRACE;
{ вышеприведенной инструкцией мы сообщаем системе, что хотим получать по событию (EXCEPTION_SINGLE_STEP) после каждой исполненной в отлаживаемом процессе машинной команды. кстати, значение константы EFLAGS_TRACE = $100 }
Context.ContextFlags := CONTEXT_CONTROL;
SetThreadContext(CurThread, Context);
{ установим новое значение регистров потока }
end;
end;
EXCEPTION_SINGLE_STEP:
{ выполнена одна машинная команда. скорее всего, возниконовение этого события - результат выполнения нашей точки останова, но кто знает? проверим флажки. если надо - восстановим точку останова }
begin
dwContinueStatus := DBG_CONTINUE;
{ скажем системе, что это исключение мы обработали сами, пусть не напрягается }
Context.ContextFlags := CONTEXT_CONTROL;
GetThreadContext(CurThread, Context);
if RestoreBreak and (Context.EIP >= BPAddr) and (Context.EIP <= BPAddr + 32) then
begin
{ это действительно "наше" событие. восстановим точку останова, чтобы перехватчик работал и дальше }
OrigByte := WriteInt3(HProc, BPAddr);
RestoreBreak := False;
Context.EFlags := Context.EFlags and not EFLAGS_TRACE;
{ сбросим флажок трассировки, ибо больше это событие нам не надо }
end
else
if RestoreBreak then
Context.EFlags := Context.EFlags or EFLAGS_TRACE;
{ вернем флажок трассировки, если событие не наше - нам ведь надо нашего дождаться. у меня система сама скидывает сей флаг, так что на всякий случай... }
Context.ContextFlags := CONTEXT_CONTROL;
SetThreadContext(CurThread, Context);
end;
end;
end;
if not ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId, dwContinueStatus) then break;
{ все. смело позволяем отлаживаемому процессу исполняться дальше }
until False;
{ сюда мы попадем только при каком-нибудь сбое или завершении процесса. на всякий случай (по совету SleepyHead'а) проверим: а точно наш отлаживаемый процесс завершился? если нет - прибьем руками }
if not ProcessFinished then
begin
repeat
TerminateProcess(HProc, RetAddr);
if not WaitForDebugEvent(Event, INFINITE) then break;
if (Event.dwDebugEventCode = EXIT_PROCESS_DEBUG_EVENT) then break;
if not ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId, DBG_CONTINUE) then break;
until False;
ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId, DBG_CONTINUE);
end;
{ все. закончили :-) }
end;
{ а вот процедурка, которая устанавливает точку останова }
procedure ProcessDLLExport(PrcH, Base: DWORD);
var
DLLName: string;
ExpTbl: TExportHeader;
N: DWORD;
begin
if (BPAddr <> 0) then exit;
{ если уже установлена - не делать ничего }
if not FindExportTable(PrcH, Base, ExpTbl) then exit;
{ если не смогли найти в DLL'ке таблицу экспорта (мало ли...) - тоже ничего не делать }
DLLName := ANSILowerCase(GetASCIIZString(PrcH, ExpTbl.NameRVA + Base));
{ получили имя DLL'ки }
if (DLLName <> 'kernel32.dll') then exit;
{ не наша? если да - снова не делаем ничего }
N := FindExportIndexByName(PrcH, Base, 'AllocConsole', ExpTbl);
N := FindExportByIndex(PrcH, Base, N, ExpTbl);
{ нашли по таблице экспорта точку входа (если не нашли - опять же ничего делать не надо }
if (N = 0) then exit;
{ а если нашли - запомним необходимую информацию и установим останов }
BPAddr := N;
OrigByte := WriteInt3(PrcH, N);
{ WriteInt3 просто возвращает в качестве результата старый байтик, и на его место записывает код $CC - инструкция Int3. когда система встречает эту инструкцию, она генерирует исключение EXCEPTION_BREAKPOINT }
end;

Все. Не так страшен черт, как его малюют [Енота: или: не так страшен Гейтс... :-)]. Остались мелочи.
Если вы запускаете процесс сами, не забудьте указать в CreateProcess флажок DEBUG_ONLY_THIS_PROCESS, чтобы отладчик мог работать, и чтобы процессы, которые может запустить отлаживаемая программа не отлаживались нами (а зачем нам дочерние процессы? если хотим перехватывать вызовы и в них, проще будет ловить непосредственно CreateProcess, и для каждого "новорожденного" запускать свою копию отладчика. Тем более, что если мы присоединяемся к уже запущенному процессу, то система по умолчанию ставит флажок DEBUG_ONLY_THIS_PROCESS. Так что перехватывать CreateProcess надежнее).
Если же вы хотите присоединиться к уже запущенному процессу, то узнайте его Id (с помощью TaskManager в NT или программно), и смело пишите DebugActiveProcess(ProcessId). В дальнейшем никаких различий между работой с процессом, запущенным нами и процессом, к которому мы присоединились "на лету" уже нет.
И еще: учтите, что если наш отладчик завершится, то система автоматически прибьет и процесс, который мы имели счастье отлаживать. Способа "отсоединиться" от процесса нет: взялся за гуж, не говори, что не дюж. :-)
Также замечу, что полезно обрабатывать возможные ошибки при вызове системных функций. Здесь я их - в основном - смело игнорирую, но вам бы лучше так не поступать.
Полные рабочие исходники можно взять с нашего сайта: http://www.piranha-home.org./Если кто-то поможет в деле перевода статьи на английский - буду очень благодарен.

Автор: Baa 3.9.2003, 01:07
dsergey, не надо уже перехватывать... надо пакет разобрать...
Please, next time read more carefully.

Автор: <Spawn> 3.9.2003, 07:00
Цитата(Baa @ 2.9.2003, 06:00)
Могу предложить дизассемблировать сервак (или оттрейсить) и посмореть, как он сам пакеты разбирает.

Можно попробовать, хотя в асме я не очень еще. Ну да ладно, где там мой IDA smile.gif

Автор: man2002ua 3.9.2003, 08:49
<Spawn>, привет smile.gif Давно не виделись!
Так ты поборол проблему перехвата в XP? Что было-то?

Насчет формата пакетов - в инете нет описалова?

Автор: <Spawn> 3.9.2003, 10:10
man2002ua Привет)))) А было то, что при подцеплении ДЛЛ я использовал hInstance(который описан в SysInit), а нужно было его внутри приложения определить - GetModuleHandle(nil). И еще немножко модернизировать класс пришлось. Так как он был расчитан на перехват АПИ в чужих приложениях, то пришлось добавть сниятие защиты - VirtualProtect. Описание вряд ли можно достать. Нивал даже исходники за $ не хочет продавать(так мне этот админ говорил). Ну думаю попытаться можно) Уже видно, то что первый байт это идентификатор пакета, потому что все пакеты начинаются так(по байтам):
19 - к примеру
0
0
0
0
0
1
...

Автор: man2002ua 3.9.2003, 12:15
огромный труд... CRC, TimeStamp, PrevPacketId ;) Различные контрольные проверки валидности пакета... удачи. Наверно у тебя вагоны времени.

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)