Сейчас на форуме: localhost1, vsv1, asfa, tyns777 (+5 невидимых)

 [email protected] —› Вопросы новичков —› Заморозка чужого процесса и работа с его памятью
. 1 . 2 . >>
Посл.ответ Сообщение

Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 29 июня 2019 20:23
· Личное сообщение · #1

В чужой программе есть два .dll файла. Нужно как-то заморозить процесс и вырезать эти .dll файлы. Словно все это делается ручками в ProcessHacker'е. Как это можно сделать на C++? Никак не могу найти подходящей информации, видимо заходить надо издалека. Направьте меня пожалуйста. Заранее спасибо!)




Ранг: 69.9 (постоянный), 83thx
Активность: 0.140.73
Статус: Участник

Создано: 29 июня 2019 21:29
· Личное сообщение · #2

Не уверен, правильно ли я понял вопрос, но - "заморозить процесс" - см. WinApi SuspendThread (перечислить все потоки в процессе и приостановить).
Память - в книге Рихтера были примеры работы с памятью , как раз таки на плюсах (простая программа для просмотра памяти процесса, см. книгу Windows via C++) , вот еще есть статья , там дельфи, но суть таже http://alexander-bagel.blogspot.com/2013/11/pmm2.html

| Сообщение посчитали полезным: hipp0gryph


Ранг: 324.3 (мудрец), 222thx
Активность: 0.480.37
Статус: Участник

Создано: 29 июня 2019 21:47 · Поправил: DenCoder
· Личное сообщение · #3

Что значит вырезать? Если нужны dll-файлы, просто сдампить их. Если исключить из памяти процесса, то примерно так

Для 1го есть дамперы. Нужно на c++ написать? Обращайтесь в пм, поделюсь кодом.

-----
IZ.RU


| Сообщение посчитали полезным: hipp0gryph


Ранг: 681.5 (! !), 405thx
Активность: 0.420.21
Статус: Участник
ALIEN Hack Team

Создано: 30 июня 2019 00:35
· Личное сообщение · #4

NtSuspendProcess засаспендит весь процесс. Как Process Explorer из Sysinternals делает. С "вырезать" надо бы уточнить, это что значит?

-----
Stuck to the plan, always think that we would stand up, never ran.


| Сообщение посчитали полезным: hipp0gryph


Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 30 июня 2019 00:36 · Поправил: difexacaw
· Личное сообщение · #5

Дампить нужно до инит. модулей, те до записи в них вне загрузчика.

В общем же никаких проблем не должно с этим возникать, разница между файловым образом и в памяти - секционное выравнивание.

Очень не обычно, если используется загрузка длл из памяти. Это делает исключительно малварь, при этом не плохая.

Добавлено спустя 33 минуты
ARCHANGEL

Заморозка потоков не приемлема для дампа. Это значит что модуль уже инициализирован и при перезапуске будет не рабочим.

Нужно отслеживать запись в модуль. Возможны два варианта, тс не достаточно инфы предоставил. Это загрузка из памяти или это файловая загрузка.

Скорее всего загрузка из памяти, так как файла нет. При этом так же вероятно что нет пе-хидера.

Если в случае с файловым образом его можно дампить до любой записи в него, те ядро замапило и пока загрузчик не поменял образ он сырой. То в случае с загрузкой из памяти всё сложнее. Нужно как минимум определить два события - окончание формирования образа и начало исполнения в нём, если первое событие скипнуто.

Нужно больше данных про загрузку.

-----
vx




Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 30 июня 2019 17:23
· Личное сообщение · #6

Ого!) Спасибо огромное за ответы!!!))) Там просто процесс подзагружает две dll'ки. И убить их можно как угодно. Это кривая защита игры, которая на каких-то пк работает, на каких-то нет. И чтобы от неё избавиться, достаточно просто из процесса их вырезать. Я никогда не занимался такими вещами, а тут стало интересно)))
difexacaw, спасибо за ответ! Как я понимаю, защита у игры ультра кривая и поэтому разрабы сделали так, что если в отношении защиты идут какие-то ошибки, то игра просто их проглатывает и забивает, поэтому убивать участки памяти которые заняты этой защитой можно целиком и все будет хорошо)) Мне демонстрировали это на примере Process Hacker 2. Просто вырезали два участка памяти занятые этими dll'ками и было все прекрасно!) Защита падала, игра работала.
ARCHANGEL, шикарно! Спасибо!) В отношении вырезать, нужно освободить участки памяти в процессе, которые постоянно занимают два этих файла.
DenCoder, спасибо!) Мне нужно исключить из памяти процесса, постараюсь разобраться, если ничего не получится постучусь!!))) Спасибо!)
morgot, Благодарю!) Попробую если не получится с NtSuspendProcess. Почитаю раздел с памятью)
Всем огромное спасибо, буду пробовать) Как что-то получится, отпишусь сюда с кодом)




Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 30 июня 2019 20:44
· Личное сообщение · #7

hipp0gryph

Как это вырезать ?
Туда будет передано управление, а если там кода нет, то дальше ошибка возникнет, которую обработать некому - код вырезан". В таком случае зачем вообще что то замораживать, это не на что не повлияет.

-----
vx




Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 30 июня 2019 21:27
· Личное сообщение · #8

difexacaw, я пытаюсь программно реализовать обход защиты, который мне показывали) Там просто заходили в Process Hacker 2, открывали процесс. Отображались различные модули, файлы и dll'ки подключенные к процессу. И отображались адреса с которых начинался каждый файл, модуль, dll. Суть обхода была заморозить процесс, выделить эти две dll там и просто убить их нажатием на Terminate. Как я понимаю, это просто вырезался кусок кода. Почему не было исключений? Потому что это защита и она очень кривая, вплоть до того, что может не работать прямо совсем. И что происходит с исключениями, я не знаю. Их просто нет. Может в других местах программы в отношении этих двух файлов идет обработка. Вообще хз) Я честно признаться не до конца понимаю как это вообще устроено. Но вот что имеем, собственно)




Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 30 июня 2019 21:33 · Поправил: difexacaw
· Личное сообщение · #9

hipp0gryph

> убить их нажатием на Terminate.

Может поток завершить, который встал в области, а не убирать области из памяти ?

Тогда понятно зачем остановка.

Покажите семпл, если размер не особо большой.

-----
vx




Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 30 июня 2019 21:56
· Личное сообщение · #10



Добавлено спустя 7 минут
Code:
  1. #include <iostream>
  2. #include <windows.h>
  3. #include <string.h>
  4. #include <Tlhelp32.h>
  5.  
  6. using namespace std;
  7.  
  8. typedef LONG(NTAPI *NtSuspendProcess)(IN HANDLE ProcessHandle);
  9. typedef LONG(NTAPI *NtResumeProcess)(IN HANDLE ProcessHandle);
  10.  
  11. void ProcessSuspend(DWORD PID)//остановка процесса
  12. {
  13.          HANDLE hProcess = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, PID);
  14.          if (!hProcess) {
  15.                  printf("Ошибка открытия процесса\n");
  16.                  return;
  17.          }
  18.          NtSuspendProcess _NtSuspendProcess = (NtSuspendProcess)GetProcAddress(GetModuleHandle("ntdll"), "NtSuspendProcess");
  19.          _NtSuspendProcess(hProcess);
  20.          CloseHandle(hProcess);
  21. }
  22.  
  23. void ProcessResume(DWORD PID)//вернуть процесс
  24. {
  25.          HANDLE hProcess = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, PID);
  26.          if (!hProcess) {
  27.                  printf("Ошибка открытия процесса\n");
  28.                  return;
  29.          }
  30.          NtResumeProcess _NtResumeProcess = (NtResumeProcess)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtResumeProcess");
  31.          _NtResumeProcess(hProcess);
  32.          CloseHandle(hProcess);
  33. }
  34.  
  35. unsigned long PIDByName(string AProcessName, DWORD pid[])//определение PID
  36. {
  37.          HANDLE pHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  38.          PROCESSENTRY32 ProcessEntry;
  39.          unsigned long i = 0;
  40.          ProcessEntry.dwSize = sizeof(ProcessEntry);
  41.          bool Loop = Process32First(pHandle, &ProcessEntry);
  42.  
  43.          while (Loop)
  44.          {
  45.                  if (ProcessEntry.szExeFile == AProcessName)
  46.                  {
  47.                         pid[i] = ProcessEntry.th32ProcessID;
  48.                         i++;
  49.                  }
  50.                  Loop = Process32Next(pHandle, &ProcessEntry);
  51.          }
  52.          CloseHandle(pHandle);
  53.          return i;
  54. }
  55.  
  56. int main()
  57. {
  58.          setlocale(LC_ALL, "Rus");
  59.          DWORD pid[10];
  60.          unsigned long size;
  61.          size=PIDByName("Launch.exe", pid);
  62.          cout << "Количество окон: " << size << endl;
  63.          for (int i = 0; i < size; i++)
  64.          {
  65.                  cout << "ID окна " << i + 1 << ": " << pid[i] << endl;
  66.          }
  67.          cin >> size;
  68.          return 0;
  69. }





Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 30 июня 2019 22:03
· Личное сообщение · #11

hipp0gryph

Список потоков, стартовые их адреса. Что дальше, те зачем эта инфа ?

-----
vx




Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 30 июня 2019 22:03
· Личное сообщение · #12

такой код пока что получается, который может успешно морозить)

Добавлено спустя 1 минуту
mrac.dll там две штуки. вот эти два потока надо завершить и все.




Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 30 июня 2019 22:13
· Личное сообщение · #13

hipp0gryph

Тоесть уже память трогать не нужно, а завершать потоки в нужных модулях.. как быстро всё меняется, никакой хрустальный шар не успеет

Если в произвольный момент остановить поток, то скорее всего его адрес окажется в сервисе, точнее на адресе возврата в нэйтивный шлюз. Тогда вам нужно крутить цикл саспенд-ресум, до тех пор пока указатель не окажется в искомой области. По нормальному же это делается иначе - блокируется память от исполнения и в ловушке киляйте потоки. Ну или вообще прикончить треды, если у них прямые стартап адреса в целевые модуля.

Есть один важный нюанс. Если в произвольный момент прикончить тред, то это скорее всего приведёт к деадлоку, апп повиснет. Так как захваченные тредом ресурсы не будут освобождены.

-----
vx




Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 30 июня 2019 22:26 · Поправил: hipp0gryph
· Личное сообщение · #14

difexacaw, ладно, поищу функции, попробую. Спасибо!)))

Добавлено спустя 55 минут
блин, не могу найти нигде как работать с чужими потоками(




Ранг: 681.5 (! !), 405thx
Активность: 0.420.21
Статус: Участник
ALIEN Hack Team

Создано: 30 июня 2019 23:23
· Личное сообщение · #15

hipp0gryph

Вам нужно:
1. Остановить процесс.
2. Перечислить все потоки и найти для них Win32StartAddress.
3. Перечислить все динамические библиотеки и найти среди них ImageBase и ImageSize для mrac.dll.
4. Для всех потоков, Win32StartAddress которых попадает в диапазон ImageBase <= x <= ImageBase + ImageSize, сделать TerminateThread.
5. Возобновить работу процесса.

Как искать Win32StartAddress: --> StackOverflow <--
По остальному - либо советовали выше, либо всё тривиально.

-----
Stuck to the plan, always think that we would stand up, never ran.





Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 30 июня 2019 23:28 · Поправил: difexacaw
· Личное сообщение · #16

ARCHANGEL

> 4. Для всех потоков, Win32StartAddress которых попадает в диапазон ImageBase <= x <= ImageBase + ImageSize, сделать TerminateThread.

Не корректное решение. Это повесит апп.

Необходимо обождать когда поток выйдет на синхро-безопасное место(иначе другой поток войдёт в бесконечный цикл ожидания глобального синхро ресурса). Это не простая задача, в простейшем случае чем меньше число стековых фреймов, тем меньше вероятность блокировки. Проще блокировать потоки до их запуска.

-----
vx


| Сообщение посчитали полезным: hipp0gryph

Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 01 июля 2019 01:43
· Личное сообщение · #17

Попытался искать эти модули внутри процесса, но не получается сделать снапшот и из-за этого ничего не выводится. В чем может быть проблема?
Code:
  1. void GetModuleListByPID(DWORD pid)
  2. {
  3.          HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
  4.          if (snapshot != INVALID_HANDLE_VALUE) 
  5.          {
  6.                  MODULEENTRY32 module;
  7.                  module.dwSize = sizeof(MODULEENTRY32);
  8.                  Module32First(snapshot, &module);
  9.  
  10.                  do {
  11.                         cout <<"Базовый адрес: " <<module.modBaseAddr<<"Имя: "<<module.szExePath;
  12.                  } while (Module32Next(snapshot, &module));
  13.          }
  14.  
  15.          CloseHandle(snapshot);
  16. }





Ранг: 681.5 (! !), 405thx
Активность: 0.420.21
Статус: Участник
ALIEN Hack Team

Создано: 01 июля 2019 14:47
· Личное сообщение · #18

hipp0gryph
Дебаг привилегии нужно получить. А так, чтобы не гадать, выводите хотя бы код ошибки через GetLastError().

difexacaw
Вы не читаете тред, что ли. Эти потоки защиты, вероятно, ни на каких ресурсах игры не завязаны и вообще стейтлесс. В худшем случае немного ресурсов утечёт (которые юзала сама защита), но это - небольшая цена за снятие защиты. Плюс защита, если бы работала, сама бы их юзала с некоей пользой, а так они просто будут висеть без дела.

-----
Stuck to the plan, always think that we would stand up, never ran.


| Сообщение посчитали полезным: hipp0gryph

Ранг: 160.9 (ветеран), 1thx
Активность: 0.050
Статус: Участник

Создано: 01 июля 2019 15:31 · Поправил: Cigan
· Личное сообщение · #19

hipp0gryph
Ну так может для тебя будет проще лоадер написать с длл, которая будет перехватывать loadlibrary и не давать грузить эту длл как таковую?



Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 01 июля 2019 18:20
· Личное сообщение · #20

Cigan, хотел защитить этот обход от распространения. Боюсь инжект dll не очень надежно, и обход заберут себе в копилку.
ARCHANGEL, 6: Неверный дескриптор.




Ранг: 69.9 (постоянный), 83thx
Активность: 0.140.73
Статус: Участник

Создано: 01 июля 2019 19:13
· Личное сообщение · #21

hipp0gryph
CreateToolhelp32Snapshot возвращает неверный дескриптор (0xff..), или getlasterror?
Просто у меня ваш код работает.
Разрядность совпадает (т.е. обе программы 32 или 64 битные)?




Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 01 июля 2019 19:26
· Личное сообщение · #22

По идеи не может быть ERROR_INVALID_HANDLE. Если процесс не откроется, будет ACCESS_DENIED. Иначе если не верный PID, должно вернуть INVALID_PARAMETER, так как PID это не описатель.

-----
vx





Ранг: 69.9 (постоянный), 83thx
Активность: 0.140.73
Статус: Участник

Создано: 01 июля 2019 19:59
· Личное сообщение · #23

difexacaw
да, я перепутал с INVALID_HANDLE_VALUE. В любом случае, код выше у меня работает, мб там прога с какой-то защитой или прав нет или разрядность не та. Ждем ТС.



Ранг: 43.1 (посетитель), 20thx
Активность: 0.160.29
Статус: Участник

Создано: 01 июля 2019 20:15
· Личное сообщение · #24

ERROR_INVALID_HANDLE может давать Module32First и CloseHandle, скорей всего ТС где-то не там вызывает GetLastError, это вполне реально особенно если присмотреться к коду: проверки выполнения Module32First нет, CloseHandle вызывается даже если дескриптор неверен.




Ранг: 337.5 (мудрец), 348thx
Активность: 2.112.42
Статус: Участник

Создано: 01 июля 2019 20:43
· Личное сообщение · #25

В данном случае лучше всего использовать отладчик, он будет получать события запуска потоков и загрузку, а есчо запустится при запуске апп --> Link <--

-----
vx




Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 11 апреля 2020 15:18 · Поправил: hipp0gryph
· Личное сообщение · #26

Спасибо огромное всем за помощь!!!) Понадобилось срочно уехать, а потом все было занято делами. Теперь пока что карантин вернулся к рассмотрению данного вопроса. Вообщем я начал морозить потоки все и сразу. Заморозил потоки > бот изменяет значение по адресу > размораживаю потоки > PROFIT!!! Защита не ругается, а в игре все крутенько. Вот что у меня получилось.
Code:
  1. #include <windows.h>
  2. #include <tlhelp32.h>
  3. #include <string>
  4. #include <iostream>
  5.  
  6. using namespace std;
  7.  
  8. void suspend(DWORD processId)
  9. {
  10.          HANDLE hThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  11.  
  12.          THREADENTRY32 threadEntry;
  13.          threadEntry.dwSize = sizeof(THREADENTRY32);
  14.          Thread32First(hThreadSnapshot, &threadEntry);
  15.          do
  16.          {
  17.                  if (threadEntry.th32OwnerProcessID == processId)
  18.                  {
  19.                               HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
  20.                               ResumeThread(hThread);
  21.                               CloseHandle(hThread);
  22.                         }
  23.          } while (Thread32Next(hThreadSnapshot, &threadEntry));
  24.          CloseHandle(hThreadSnapshot);
  25. }
  26.  
  27. DWORD FindProcessId(string processName)
  28. {
  29.          PROCESSENTRY32 peInfo;
  30.          peInfo.dwSize = sizeof(peInfo);
  31.          HANDLE _hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
  32.          if (_hSnap == INVALID_HANDLE_VALUE)
  33.                  return 0;
  34.          Process32First(_hSnap, &peInfo);
  35.          if (!processName.compare(peInfo.szExeFile))
  36.          {
  37.                  CloseHandle(_hSnap);
  38.                  return peInfo.th32ProcessID;
  39.          }
  40.          while (Process32Next(_hSnap, &peInfo))
  41.          {
  42.                  if (!processName.compare(peInfo.szExeFile))
  43.                  {
  44.                         CloseHandle(_hSnap);
  45.                         return peInfo.th32ProcessID;
  46.                  }
  47.          }
  48.          CloseHandle(_hSnap);
  49.          return 0;
  50. }
  51.  
  52.  
  53.  
  54. int main() {
  55.          HANDLE _hProc = NULL;
  56.          DWORD _pID = NULL;
  57.          _pID = FindProcessId("Name.exe");
  58.          cout << _pID << endl;
  59.          suspend(_pID);
  60.          return 0;
  61. }


Добавлено спустя 24 минуты
Введение:
Теперь перешагнув через это хочу создать собственный полноценный чит, коего там ещё никогда не было, с этим недо обходом)
Через Cheat Engine собрал много полезных данных, которые позволяют мне сделать в игре нечто новое, что никто не разрабатывал и это будет там очень к месту.
Проблема:
Пытаюсь реализовать чтение адреса с помощью указателя. Нашел базовый адрес, оффсеты.

Теперь нужно как-то реализовать это программно. Прочитал статьи по работе с базовым адресом и оффсетами. Понял что это такое.
У меня выстроена цепочка из указателей. Получается я должен взять из базового адреса значение - это будет первый указатель, после прибавить к этому указателю первый оффсет, посмотреть значение - это второй указатель, ко второму прибавить оффсет - получить значение из третьего и т.д. Правильно ли я понял принцип? Что не так с моим кодом? Если я что-то не знаю или не правильно понял, посоветуйте пожалуйста хорошую статью. Заранее спасибо!!! Пока получается нерабочее вот это:
Code:
  1.          DWORD base = 0x10070E98;
  2.          DWORD offset;
  3.          int num=0;
  4.          while (true) {
  5.                  ReadProcessMemory(_hProc, (PBYTE*)base, &offset, sizeof(int), 0);
  6.                  ReadProcessMemory(_hProc, (PBYTE*)offset + 0xA0, &offset, sizeof(int), 0);
  7.                  ReadProcessMemory(_hProc, (PBYTE*)offset + 0x10, &offset, sizeof(int), 0);
  8.                  ReadProcessMemory(_hProc, (PBYTE*)offset + 0x24, &offset, sizeof(int), 0);
  9.                  ReadProcessMemory(_hProc, (PBYTE*)offset + 0x0, &offset, sizeof(int), 0);
  10.                  ReadProcessMemory(_hProc, (PBYTE*)offset + 0x28, &num, sizeof(int), 0);
  11.                  
  12.                  cout << num << endl;
  13.                  Sleep(5000);
  14.                  system("cls");
  15.          }
  16.          return 0;
  17. }




Ранг: 7.1 (гость), 6thx
Активность: 0.040.01
Статус: Участник

Создано: 11 апреля 2020 19:04
· Личное сообщение · #27

Принцип верный, но с адресами что-то не то. Откуда взялось 10070E98? Я там вижу смещение 00070E98, и к чему это значение прибавляется, не понятно



Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 11 апреля 2020 19:16
· Личное сообщение · #28

Я думал базовый адрес именно этот пробил вот по значению. Или что-то не то?


Добавлено спустя 1 минуту
Я не понимаю как hex код грамотно принять и вывести. Обычный cout<<hex<<offset<<endl; не прет

Добавлено спустя 2 минуты
Думаю если бы получалось выводить, дело быстрее бы пошло, но подозреваю что как-то не так получаю.



Ранг: 431.7 (мудрец), 390thx
Активность: 0.730.32
Статус: Участник

Создано: 11 апреля 2020 20:06 · Поправил: dosprog
· Личное сообщение · #29

hipp0gryph пишет:
Я не понимаю как hex код грамотно принять и вывести. Обычный cout<<hex<<offset<<endl; не прет

{
char hex_string[0x20]="";
sprintf(&hex_string[0],"%0X",offset);
cout<<hex_string<<endl;
}



Ранг: 1.6 (гость)
Активность: 0.030.01
Статус: Участник

Создано: 11 апреля 2020 20:14
· Личное сообщение · #30

'%0X' подчеркивает и пишет аргумент типа int несовместим с const char *

Добавлено спустя 7 минут
все, разобрался, спасибо) DWORD спокойно выводится в нужном виде с помощью cout<<hex<<offset<<endl; Просто не хватало прав


. 1 . 2 . >>
 [email protected] —› Вопросы новичков —› Заморозка чужого процесса и работа с его памятью
:: Ваш ответ
Жирный  Курсив  Подчеркнутый  Перечеркнутый  {mpf5}  Код  Вставить ссылку 
:s1: :s2: :s3: :s4: :s5: :s6: :s7: :s8: :s9: :s10: :s11: :s12: :s13: :s14: :s15: :s16:


Максимальный размер аттача: 500KB.
Ваш логин: german1505 » Выход » ЛС
   Для печати Для печати