Введение в Landlock

Введение в Landlock


В предыдущей статье мы показали, как можно сделать приложения безопаснее с помощью seccomp, который позволяет ограничивать список системных вызовов, доступных процессу или потоку. В этот раз разберём, как использовать Landlock LSM, чтобы ещё сильнее поднять уровень безопасности приложения.

Вступление: что такое Landlock?

Landlock - это модуль безопасности Linux, который даёт возможность ограничивать доступ к файловой системе. Если обратиться к документации, там говорится:

Цель Landlock - дать возможность ограничивать «окружающие» права (например, глобальный доступ к файловой системе) для набора процессов. Поскольку Landlock - стекуемый LSM, он позволяет создавать безопасные песочницы как дополнительные уровни защиты поверх существующих системных механизмов контроля доступа. Такие песочницы помогают уменьшить влияние уязвимостей или неожиданных/вредоносных действий в пользовательских приложениях. Любой процесс, включая непривилегированный, может самостоятельно и безопасно сузить свои права.

Если сказать проще, Landlock позволяет пользовательскому приложению создать набор правил, который будет ограничивать его доступ к файловой системе. Чтобы воспользоваться Landlock, приложение сначала должно сформировать ruleset - набор правил, описывающий, что ему разрешено. Доступные типы разрешений и их смысл разобраны в документации.

// Define a new ruleset
struct landlock_ruleset_attr ruleset_attr = {
    .handled_access_fs =
        LANDLOCK_ACCESS_FS_EXECUTE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_READ_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_FILE |
        LANDLOCK_ACCESS_FS_MAKE_CHAR |
        LANDLOCK_ACCESS_FS_MAKE_DIR |
        LANDLOCK_ACCESS_FS_MAKE_REG |
        LANDLOCK_ACCESS_FS_MAKE_SOCK |
        LANDLOCK_ACCESS_FS_MAKE_FIFO |
        LANDLOCK_ACCESS_FS_MAKE_BLOCK |
        LANDLOCK_ACCESS_FS_MAKE_SYM |
        LANDLOCK_ACCESS_FS_REFER |
        LANDLOCK_ACCESS_FS_TRUNCATE,
};

// Call into the kernel to create the ruleset
int ruleset_fd = syscall(SYS_landlock_create_ruleset,
                    &ruleset_attr, sizeof(ruleset_attr));

Теперь можно начинать добавлять правила в этот ruleset, чтобы ограничить доступ к файловой системе. Для этого сначала создаётся структура landlock_path_beneath_attr, в которой задаются разрешения, которые вы готовы выдать процессу, и файловый дескриптор родительского каталога. После этого правило добавляется в ruleset с помощью системного вызова landlock_add_rule.

Пример:

struct landlock_path_beneath_attr path_beneath = {
    .allowed_access =
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_READ_DIR,
};

path_beneath.parent_fd = open("/my_app_data", O_PATH | O_CLOEXEC);

err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
                        &path_beneath, 0);
close(path_beneath.parent_fd);

Дальше остаётся только загрузить ruleset в ядро - и всё готово.

// forbid this thread from getting new privileges
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));

landlock_restrict_self(ruleset_fd, 0));
close(ruleset_fd);

Пример уязвимого Go-приложения

Теперь посмотрим, как можно применить Landlock, чтобы защитить уязвимое Go-приложение. Допустим, у нас есть такая программа (это просто учебный пример, не предназначенный ни для продакшена, ни как образец для собственных приложений):

package main

import (
 "io"
 "log"
 "net/http"
 "os"
)

func main() {

 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  log.Println(r.RequestURI)

  path := r.URL.Query().Get("path")

  f, err := os.Open(path)
  if err != nil {
   log.Println(err)
   w.WriteHeader(http.StatusNotFound)
   return
  }
  defer f.Close()

  buf, err := io.ReadAll(f)
  if err != nil {
   log.Print(err)
   w.WriteHeader(http.StatusInternalServerError)
   return
  }

  w.Write(buf)
 })

 http.ListenAndServe(":9999", nil)
}

Это приложение просто читает значение параметра path из запроса, открывает соответствующий файл и отдаёт клиенту его содержимое. Проблема очевидна: можно прочитать любой файл в системе. Например:

$ curl http://localhost:9999/?path=/etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

Смягчаем уязвимость с помощью Landlock

Теперь мы собираемся подпилить приложение так, чтобы оно использовало Landlock и имело доступ только к каталогу, где лежат его данные. Для простоты это будет текущий каталог, из которого мы его запускаем.

func main() {

 err := landlock.V3.BestEffort().RestrictPaths(
  landlock.RODirs("."),
 )
 if err != nil {
  log.Fatal(err)
 }

 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  log.Println(r.RequestURI)

  path := r.URL.Query().Get("path")

  f, err := os.Open(path)
  if err != nil {
   log.Println(err)
   w.WriteHeader(http.StatusNotFound)
   return
  }
  defer f.Close()

  buf, err := io.ReadAll(f)
  if err != nil {
   log.Print(err)
   w.WriteHeader(http.StatusInternalServerError)
   return
  }

  w.Write(buf)
 })

 http.ListenAndServe(":9999", nil)
}

Сначала мы выбираем последнюю версию ABI Landlock, то есть V3, а затем вызываем BestEffort, который вернёт максимально строгую конфигурацию для этой ABI. После этого вызываем RestrictPaths, который добавит в ruleset подходящее правило, ограничивающее доступ текущим каталогом.

Попробуем снова запустить приложение:

$ curl -v http://localhost:9999/?path=/etc/passwd
* Uses proxy env variable no_proxy == 'localhost,127.0.0.0/8,::1'
*   Trying 127.0.0.1:9999...
* Connected to localhost (127.0.0.1) port 9999 (#0)
> GET /?path=/etc/passwd HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/8.0.1
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< Date: Mon, 12 Jun 2023 11:28:10 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

В другой консоли, где наше приложение пишет логи, видим:

2023/06/12 11:20:19 /?path=/etc/passwd
2023/06/12 11:20:19 open /etc/passwd: permission denied

Как видно, приложение больше не может прочитать файл /etc/passwd. Landlock неявно смягчил уязвимость. Вместе seccomp и Landlock могут быть очень эффективной комбинацией мер безопасности для снижения рисков в приложениях, и для обоих существуют биндинги под разные языки.


Report Page