С ростом удаленной работы и распределенных рабочих сред отслеживание действий пользователей стало более сложным. eBPF предлагает мощный способ сбора данных внутри хостов и позволяет специалистам по безопасности получать представление о событиях на уровне ядра.

Сценарий: несколько системных администраторов подключены к одному хосту через SSH. Администраторы входят в систему под назначенными им пользователями. Однако некоторые системные администраторы решают «sudo su», запускать новые оболочки или выполнять ssh user@localhost. Тогда как мы можем отследить эти действия до исходного SSH-соединения? Кроме того, если это злонамеренные пользователи, пытающиеся скрыть свои первоначальные удаленные соединения, как мы можем эффективно отследить это в момент возникновения?

Поскольку сотни удаленных пользователей подключаются к серверам, хостам и виртуальным машинам организаций, нелегко отследить пользователей до их первоначальных подключений. Таким образом, мы создали агент, позволяющий легко отслеживать и регистрировать все действия хоста со всех клиентов SSH (совместим как с IPv4, так и с IPv6).

Ознакомьтесь с исходным кодом нашего проекта по этой ссылке GitHub.

eBPF

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

Отслеживание активности удаленного хоста в GNU/Linux

Мы хотели создать инструмент, который мог бы сопоставить действие с пользователем и удаленным IP-адресом в момент события. По нашему опыту, обычно анализ проводится позже. Инструменты, которые мы сейчас используем для отслеживания действий пользователей, ограничены. Эти инструменты дают нам много информации, но они не предназначены для корреляции и анализа на месте. Обычно для нас анализ происходит в системе управления информацией и событиями (SIEM), где встроенные приложения или аналитики собирают журналы для отслеживания вредоносной активности до ее источника. Таким образом, мы предположили, что анализ в SIEM требует больше вычислений и ручной работы для сопоставления активности с пользователем или удаленным IP-адресом, чем для прямой корреляции активности в момент ее возникновения. Проще говоря, базовый ручной анализ потребует:

  1. Найдите идентификатор процесса (PID) и идентификатор пользователя (UID) действия (например, ps -elf).
  2. Найдите идентификатор родительского процесса (PPID) для родительского процесса и рекурсивно найдите оболочку (bash) (т. е. отслеживание дерева процессов).
  3. Сопоставьте этот PPID с активным сеансом SSH.
  4. Поиск IP-адреса и порта удаленного клиента путем просмотра сеанса SSH (т. е. ss -auntp)

Выявление проблемы

Мы заметили, что при использовании ручного анализа, описанного выше, возникает уникальная проблема, когда удаленный пользователь решает «локально» выполнить SSH для другого пользователя, повысить привилегии или создать новые оболочки. Выполнив описанные выше действия, вы увидите в журналах, что этот удаленный пользователь соединился с «127.0.0.1» с неизвестным исходным пользователем; однако журналы не сразу сообщат вам исходный удаленный IP-адрес без дальнейшего анализа. Более того, вы больше не можете использовать PID для корреляции этих событий, поскольку отслеживание PID становится затруднительным, если SSH-клиент и SSH-сервер не находятся в одном дереве процессов. Из-за эфемерного характера статуса процесса (ps) и состояния сети (netstat или ss) аналитикам может быть нелегко получить информацию после завершения процесса или сеанса (если только эти выходные данные не фиксируются постоянно). Таким образом, мы придумали способ отслеживать всю информацию о «первом» сеансе SSH через все уровни «локального» SSH, повышения привилегий и новой оболочки в момент его возникновения, чтобы сохранить целостность информации.

Метод

Мы использовали strace, чтобы углубиться в процесс sshd, когда клиент подключается к серверу. Мы видели, что вызывается системный вызов: getpeername и getsockname. Эти системные вызовы содержат дескриптор файла сокета, IPv4|IPv6, порт и IP-адрес. Мы хотели использовать эту информацию для отслеживания сеансов SSH.

Базовая структура

Настройка

  1. Создание экземпляров карт bpf
struct {
  __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
  __uint(key_size, sizeof(u32));
  __uint(value_size, sizeof(u32));
} output SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, pid_t);
  __type(value, struct sockaddr_in6 *);
} values SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, pid_t);
  __type(value, struct ipData);
  __uint(pinning, LIBBPF_PIN_BY_NAME);
} addresses SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, pid_t);
  __type(value, struct event);
  __uint(pinning, LIBBPF_PIN_BY_NAME);
} execs SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, pid_t);
  __type(value, struct sockaddr_in6 *);
  __uint(pinning, LIBBPF_PIN_BY_NAME);
} raw_sockaddr SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, uint16_t);
  __type(value, struct ipData);
  __uint(pinning, LIBBPF_PIN_BY_NAME);
} raw_port SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, pid_t);
  __type(value, uid_t);
  __uint(pinning, LIBBPF_PIN_BY_NAME);
} raw_user SEC(".maps");

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, 10240);
  __type(key, uint16_t);
  __type(value, uid_t);
  __uint(pinning, LIBBPF_PIN_BY_NAME);
} raw_userport SEC(".maps");

2. Подключите точки трассировки к sys_enter_getpeername, sys_exit_getpeername, sys_enter_getsockname, sys_exit_getsockname, sys_enter_execve и sys_exit_execve.

SEC("tp/syscalls/sys_enter_getpeername")
int tp_sys_enter_getpeername(struct trace_event_raw_sys_enter *ctx) {
  return probe_entry_getpeername(ctx, (struct sockaddr_in6*)ctx->args[1]);
}

SEC("tp/syscalls/sys_exit_getpeername")
int tp_sys_exit_getpeername(struct trace_event_raw_sys_exit *ctx) {
  return probe_return_getpeername(ctx, (int)ctx->ret);
}

SEC("tp/syscalls/sys_enter_getsockname")
int tp_sys_enter_getsockname(struct trace_event_raw_sys_enter *ctx) {
  return probe_entry_getsockname(ctx, (struct sockaddr_in6*)ctx->args[1]);
}

SEC("tp/syscalls/sys_exit_getsockname")
int tp_sys_exit_getsockname(struct trace_event_raw_sys_exit *ctx) {
  return probe_return_getsockname(ctx, (int)ctx->ret);
}

SEC("tracepoint/syscalls/sys_enter_execve")
int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter *ctx) {
...
}

3. Настройка условий отправки событий bpf в пространство пользователя. (см. sshtrace.bpf.c)

Удаленный SSH-клиент (узкая область действия)

  1. Когда удаленный SSH-клиент успешно проходит аутентификацию по sshd, процесс sshd вызывает системный вызов getpeername, чтобы получить информацию об IP-адресе и порте удаленного клиента. Пример: «[pid 27841] getpeername(4, {sa_family=AF_INET, sin_port=htons(40146), sin_addr=inet_addr(»192.168.86.120»)}, [128 => 16]) = 0'
  2. Последний процесс клонирования sshd вызывает системный вызов execve для создания оболочки (оболочки настроены в /etc/passwd). Пример: [pid 27878] execve("/bin/bash", ["-bash"], 0x5555ffffeeee /* 16 vars */) = 0
  3. Любые команды, выполняемые из этой оболочки, будут иметь PPID, равный PID процесса sshd на шаге 2.

А как насчет «локального» удаленного SSH-клиента?

  1. Удаленный SSH-клиент уже подключен к локальному хосту и решает снова вызвать SSH, чтобы переключить пользователя.
  2. Клиентский процесс SSH создается на локальном хосте. getsocknameвызывается для получения собственного IP-адреса и порта. Убедитесь, что вы являетесь локальным хостом, и сохраните порт, чтобы сопоставить его на стороне SSH-сервера.
} else if (m->type_id == GETSOCKNAME) {
    struct ipData ipRes = ipHelper(&m->addr);
    if (!strncmp(ipRes.ipAddress, "127.0.0.1", INET_ADDRSTRLEN) ||
        !strncmp(ipRes.ipAddress, "::1", INET6_ADDRSTRLEN)) {
      int map_port =
          bpf_obj_get("/sys/fs/bpf/raw_port"); // port -> IP data 
      int userMapport =
          bpf_obj_get("/sys/fs/bpf/raw_userport"); // port -> user
      if (userErr) {
        org_user = m->uid;
      }
      if (map_port && userMapport) {
        int sockDataErr = bpf_map_lookup_elem(addrMap, &m->ppid, &sockData);
        if (sockDataErr == 0) {
          if (sockData.port == 0) {
            sockData = ipRes;
          }
        } else {
        }
        // update port with original IP data
        bpf_map_update_elem(map_port, &ipRes.port, &sockData,BPF_ANY); 
        // update port with original user
        bpf_map_update_elem(userMapport, &ipRes.port, &org_user,BPF_ANY); 
      } else {
        log_debug("Port maps file decriptors not found");
      }

3. На стороне SSH-сервера (все еще локального хоста) вызывается getpeername, чтобы получить IP-адрес и порт клиента. Убедитесь, что клиент является локальным хостом, и используйте порт клиента для поиска в хэш-карте, чтобы получить исходный IP-адрес и порт.

else if (m->type_id == GETPEERNAME) {
    struct ipData ipRes = ipHelper(&m->addr);
    if (addrMap) {
      bpf_map_update_elem(addrMap, &m->pid, &ipRes, BPF_ANY);
    }
    if (!strncmp(ipRes.ipAddress, "127.0.0.1", INET_ADDRSTRLEN) ||
        !strncmp(ipRes.ipAddress, "::1", INET6_ADDRSTRLEN)) {
      struct ipData tmpSockData;
      uid_t originalUser;
      int portMap = bpf_obj_get("/sys/fs/bpf/raw_port");                                               
      if (portMap <= 0) {
        log_debug("No file descriptor returned for the port BPF map object");
      }
      log_trace("Getting the userport BPF map object");
      int userportMap =
          bpf_obj_get("/sys/fs/bpf/raw_userport"); 
      if (userportMap <= 0) {
        log_debug(
            "No file descriptor returned for the userport BPF map object");
      }
      if (portMap && userportMap) {
        log_trace("Looking up the sockaddr_in corresponding to port %d in the "
                  "port map",
                  port);
        // look up port to get original IP data
        bpf_map_lookup_elem(portMap, &ipRes.port, &tmpSockData); 
        // look up port to get original user
        bpf_map_lookup_elem(userportMap, &ipRes.port, &originalUser); 
        // update PID with original user
        bpf_map_update_elem(userMap, &m->pid, &originalUser, BPF_ANY);
        // update PID with the original IP data
        bpf_map_update_elem(addrMap, &m->pid, &tmpSockData, BPF_ANY);
  
        
      }

4. Наконец, рекурсивно найдите PID процесса sshd, который стал оболочкой. Обновите этот PID исходными данными. Теперь эта новая оболочка будет иметь ту же информацию об IP-адресе и порте, что и исходное соединение SSH. Любые команды, запускаемые из этой оболочки (и на более глубоких уровнях), сохранят эту информацию.

while (ppid > 1 && strncmp(comm, "(sshd)", 6) != 0) {
      pid_t ancestorPID = getPPID(ppid);
      char *comm = getCommand(ancestorPID);
      if (strncmp(comm, "(sshd)", 6) == 0) {
        sshdFound = true;
        // We want the process just before sshd, i.e. ppid
        sshdPID = ppid;
        userErr = bpf_map_lookup_elem(userMap, &sshdPID,
                                      &org_user); // look up org_user
        addrErr = bpf_map_lookup_elem(addrMap, &sshdPID,
                                      &sockData); // look up IP data
        if (addrErr != 0) {
          log_trace(
              "Couldn't find a corresponding sockaddr_in for the sshd process");
        } else {
          log_trace("Found a corresponding sockaddr_in for the sshd process");
          break;
        }
      }
      ppid = ancestorPID;
    }

См. sshtrace.c для исходного кода пользовательского пространства.

Мониторинг

Поля данных

  • Отметка времени: местное время для печати и время эпохи для файла журнала.
  • PID: идентификатор процесса, вызвавшего системный вызов execve.
  • PPID: идентификатор родительского процесса.
  • Текущий пользователь: пользователь, использующий оболочку в данный момент.
  • Исходный пользователь: пользователь, который первоначально подключался через SSH к хосту.
  • Команда: двоичный файл, вызванный execve.
  • IP-адрес: исходный удаленный IP-адрес, который подключался через SSH к хосту.
  • Порт: порт SSH-клиента удаленного хоста.
  • BinPath: расположение двоичного файла.

На изображении выше вы можете видеть, что как только SSH-клиент подключается к хосту, он захватывает удаленный IP-адрес и удаленный порт (локальный порт SSH обычно равен 22). Кроме того, когда клиент выполняет sudo su,программа сохраняет исходный IP-адрес, порт и пользователя.

На изображении выше вы можете видеть, что пользователь harambe выполнил SSH для локального хоста как пользователь guac. (игнорируйте корневые действия). Вы можете видеть, что harambe вызывает ssh и входит в систему guac, а IP-адрес, порт и исходный пользователь остаются такими же, как и в исходном сеансе.

Одновременно можно отслеживать несколько пользователей. Каждый удаленный пользователь идентифицируется по IP-адресу удаленного клиента и первому пользователю, вошедшему в систему. В случае, если разные пользователи подключаются по SSH с одного и того же удаленного IP-адреса, вы можете идентифицировать разных пользователей по порту или пользователю.

Пользователи отслеживаются даже после выполнения ssh user@localhost. Это нелегко отследить, поскольку SSH-клиент и SSH-сервер находятся на одном устройстве, поскольку клиент и сервер находятся в разных деревьях процессов. Эта программа использует клиентские порты SSH для уникальной идентификации и сопоставления наличия SSH на локальном хосте.

IP-адрес, порт и пользователи хранятся в хэш-карте bpf, и доступ к ним можно получить очень эффективно и безопасно. Из-за временной уникальности PID оболочки карты гарантируют, что уникальные клиенты SSH сохранят свою собственную информацию без пересечения.

Ведение журнала

Мы создали файл журнала в /var/log/sshtrace.log, чтобы его можно было передать в SIEM для анализа и хранения.

Заключение

eBPF позволяет специалистам по безопасности настраивать способ мониторинга систем GNU/Linux. eBPF может предложить творческие способы повышения эффективности мониторинга и обнаружения. Это лишь один из многих способов использования eBPF для сокращения ручной работы в сфере кибербезопасности.

Проектная группа:

Ознакомьтесь с исходным кодом нашего проекта по этой ссылке GitHub.

Спасибо за прочтение!