소개

SekoiaTDR(위협 탐지 및 연구) 팀은 매일 다양한 환경을 추적·분석하며 새로운 탐지 기회를 찾는다. 서버 측에서 리눅스 기반 시스템이 널리 쓰이는 만큼, 이번 글에서는 최근 리눅스 보안 메커니즘인 Landlock에 초점을 맞췄다.

Landlock은 비교적 새로운 리눅스 보안 모듈(LSM)로, 리눅스 커널 5.13부터 통합됐다. 다른 LSM과의 주요 차이는 Landlock 논문의 비교표에 정리돼 있다. Landlock은 기존 시스템 전체 접근 제어 메커니즘 위에 애플리케이션용 샌드박스를 만들 수 있게 한다. 신뢰 여부나 권한 수준과 관계없이 어떤 프로세스든 제한할 수 있어 심층 방어 계층을 추가한다.

Landlock의 기능은 유망하지만 커널에 도입된 지 오래되지 않았다. 따라서 탐지 엔지니어링 관점에서는 실제로 얼마나 널리 쓰이는지가 중요한 질문이다. Landlock 제작자는 2025년 발표 자료에서 이미 Landlock을 활용하는 오픈소스 프로젝트로 zathura, pacman, cloud hypervisor, suricata, polkadot, wireproxy, gnome localsearch, xz utils를 꼽았다. FireJail과 setpriv 같은 여러 샌드박스 도구도 Landlock을 통합했다. 여러 민간 기업도 애플리케이션을 강화하기 위해 자체적으로 Landlock을 검토했다.

탐지 엔지니어링을 위한 Landlock

Landlock은 애플리케이션의 동작을 효과적으로 제한하는 강화 도구다. 하지만 시스템 보안 강화에만 쓰이지 않는다. 탐지 엔지니어링에도 유용한 정보를 제공한다.

Landlock은 리눅스 커널 6.15부터 Audit 시스템에 완전히 통합됐다. 별도의 감사 규칙은 필요하지 않다. 다만 기본 설정에서는 execve() 시스템 호출을 통한 새 프로그램 실행이 로그를 만들지 않는다. 이 동작을 켜려면 sys_landlock_restrict_self()에 특정 플래그를 전달해야 한다. 새 프로세스의 실행 흐름을 통제하기 어렵다는 점을 고려해 오탐을 줄이려는 기본 설정이다. 또한 바이너리를 컴파일할 때 공식 문서에 따라 적절한 접근 권한을 정의해야 한다.

로그가 발생했다는 것은 두 가지를 뜻한다. 첫째, 애플리케이션이 Landlock 제한을 포함해 컴파일됐으므로 원래 의도된 용도대로 사용되지 않고 있다는 뜻이다. 둘째, 애플리케이션이 처음부터 Landlock 제한을 전제로 설계됐으므로 해당 로그는 오탐일 수 없다는 설명이다. Sekoia는 이 텔레메트리를 이용하면 실행 중인 위협을 식별하는 정밀한 행위 기반 탐지 규칙을 만들 수 있고, 오탐률도 매우 낮출 수 있다고 주장한다.

작동 방식을 보여주는 리눅스 커널의 C sandboxer 예제는 사용자가 정의한 여러 조건 아래에서 프로세스를 실행한다. 이 예제로 기본적인 테스트를 수행해 로그 기능을 확인할 수 있다.

프로그램에 적용된 제한을 위반하려는 시도는 환경 변수 LL_FORCE_LOG를 1로 설정하면 모두 기록된다. 이 설정은 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON 플래그를 활성화해 execve() 시스템 호출을 통한 새 프로그램 실행을 허용한다.

파일 시스템 Landlock 규칙

먼저 sandboxer 바이너리로 대화형 bash 셸을 시작한다. Landlock으로 파일 시스템 권한을 제한해 / 경로는 읽기 전용으로, /tmp 경로는 읽기·쓰기가 가능하도록 설정한다. 원문은 이 과정을 보여주는 셸 명령을 그림 2에 제시한다.

/tmp가 아닌 다른 경로에 쓰기를 시도하면 Audit 로그에 Landlock의 문서화된 메시지가 남는다.

text
type=UNKNOWN[1423] msg=audit(1764079004.833:1261): domain=1266faee0 blockers=fs.make_reg path="/home/user/sandbox_c" dev="sda1" ino=1183972

글을 작성할 당시에는 Audit 사용자 공간 도구가 커널의 감사 메시지 유형을 아직 반영하지 않았다. 메시지 코드 1423은 다음과 같다.

c
#define AUDIT_LANDLOCK_ACCESS 1423 /* Landlock denial */

go-libaudit 라이브러리를 사용하면 감사 메시지를 재구성하고 시스템 맥락을 더해 탐지나 헌팅에 쓸 수 있는 단일 이벤트로 만들 수 있다.

json
{"@timestamp":"2025-11-25T13:46:48.03Z","sequence":1228,"category":"mac","record_type":"unknown[1423]","result":"fail","session":"unset","tags":["x86_64"],"summary":{"actor":{"primary":"unset","secondary":"user"},"object":{},"how":"/usr/bin/touch"},"user":{"ids":{"auid":"unset","egid":"1000","euid":"1000","fsgid":"1000","fsuid":"1000","gid":"1000","sgid":"1000","suid":"1000","uid":"1000"},"names":{"egid":"user","euid":"user","fsgid":"user","fsuid":"user","gid":"user","sgid":"user","suid":"user","uid":"user"},"selinux":{"user":"unconfined"}},"process":{"pid":"4480","ppid":"1767","title":"touch password.txt","name":"touch","exe":"/usr/bin/touch"},"data":{"a0":"ffffffffffffff9c","a1":"7ffd04a91331","a2":"941","a3":"1b6","arch":"x86_64","blockers":"fs.make_reg","dev":"sda1","domain":"1266faee0","exit":"EACCES","ino":"1183972","path":"/home/user/sandbox_c","syscall":"openat","tty":"pts1"}}

네트워크 Landlock 규칙

앞선 예와 마찬가지로 sandboxer 바이너리에서 대화형 bash 셸을 시작한 뒤, TCP 연결을 포트 443으로 제한한다. 원문은 이를 시험하는 셸 명령을 그림 3에 제시한다.

그런 다음 curl 명령으로 http://google.com에 연결을 시도한다. 이는 포트 80을 사용한다. IPv4 연결에서 발생한 로그는 다음과 같다.

text
type=UNKNOWN[1423] msg=audit(1764080735.711:1623): domain=1266faef9 blockers=net.connect_tcp daddr=142.250.179.110 dest=80

go-libaudit 라이브러리로 처리한 이벤트는 다음과 같다.

json
{"@timestamp":"2025-11-25T14:25:35.711Z","sequence":1623,"category":"mac","record_type":"unknown[1423]","result":"fail","session":"unset","tags":["x86_64"],"summary":{"actor":{"primary":"unset","secondary":"user"},"object":{},"how":"/usr/bin/curl"},"user":{"ids":{"auid":"unset","egid":"1000","euid":"1000","fsgid":"1000","fsuid":"1000","gid":"1000","sgid":"1000","suid":"1000","uid":"1000"},"names":{"egid":"user","euid":"user","fsgid":"user","fsuid":"user","gid":"user","sgid":"user","suid":"user","uid":"user"},"selinux":{"user":"unconfined"}},"process":{"pid":"5482","ppid":"5470","title":"curl https://google.com","name":"curl","exe":"/usr/bin/curl"},"data":{"a0":"5","a1":"5611825b7088","a2":"10","a3":"0","arch":"x86_64","blockers":"net.connect_tcp","daddr":"142.250.179.110","dest":"80","domain":"1266faef9","exit":"EACCES","syscall":"connect","tty":"pts1"}}

이러한 로그를 활용하면 탐지 엔지니어가 간단한 탐지 규칙을 만들 수 있다. Sekoia.io Endpoint Agent는 go-libaudit 라이브러리를 사용하므로 앞서 설명한 이벤트를 기본으로 수집한다. 이벤트는 Sekoia SOC 플랫폼으로 전송되며, Sigma를 사용해 탐지 규칙을 만들 수 있다. Landlock 규칙을 적용한 바이너리가 적절히 조정돼 있다는 전제에서, 수집된 이벤트에 Landlock에만 해당하는 키인 domain과 blockers가 있는지 확인한다.

Landlock 활용 사례

실행 파일의 취약점

Landlock의 주요 활용 사례는 민감한 서비스를 강화하는 것이다. 방어 담당자는 외부에 노출된 서비스에 대한 잠재적 악용 시도를 감시하고 차단하는 데 Landlock을 활용할 수 있다.

설명을 위해 사용자 제공 URI 쿼리 매개변수를 제대로 정화하지 않는 취약한 HTTP 서버를 예로 든다. 해당 코드는 부록에 있다. 취약한 코드는 요점을 보여주기 위해 의도적으로 취약하고 쉽게 수정할 수 있도록 작성한 단순한 예다.

이 웹 서버는 특정 파일 시스템 위치(/var/lib/htdocs)의 웹 페이지에만 읽기 권한을 가진다. 다른 경로로의 악의적인 접근을 막기 위한 설정이며, 코드에서는 다음 Landlock 규칙으로 적용한다.

text
const allowedDir = "/var/lib/htdocs"...landlock.V5.BestEffort().RestrictPaths( landlock.RODirs(allowedDir), )

사용자가 정의된 범위를 벗어난 경로(예: /etc/passwd)를 요청하면, 정상적인 웹 서버라면 정화해야 할 요청이라도 Landlock이 거부한다. 원문은 취약점 악용을 시도하는 셸 명령을 그림 4에, Sekoia SOC 플랫폼에서 해당 이벤트가 생성한 경보를 그림 5에 제시한다.

Sekoia는 이 호스트 수준 탐지가 네트워크 모니터링 같은 상위 계층 신호에 의존하는 것보다 효과적이라고 본다. 샌드박스 애플리케이션은 필요한 권한이 적어 공격자가 우회하기 어렵다. 또한 Landlock 로그를 통해 자동화된 스캐너의 실패한 시도만 잡는 데 그치지 않고, 실제 공격 경로가 악용되는 상황을 관찰할 수 있다고 설명한다.

XZ의 Landlock 통합

XZ Utils는 일부 tarball 버전(5.6.0과 5.6.1)에서 악성 코드가 발견된 CVE-2024-3094 사건으로 잘 알려져 있다. 관련 기사가 여럿 있으므로 여기서는 공격 전반이 아니라 한 가지 지점에 초점을 맞춘다. 공격자는 점 하나를 넣어 Landlock 코드가 컴파일되지 않게 했다. 이 공급망 공격의 전체 컴파일 과정에는 Landlock이 필요하지 않았던 것으로 보여, 공격자가 그렇게 한 이유를 입증하는 포렌식 증거는 없었다고 Sekoia는 설명한다.

XZ의 허용 규칙 목록은 다음과 같다. 그 밖의 모든 동작은 거부된다.

  • LANDLOCK_ACCESS_FS_WRITE_FILE
  • LANDLOCK_ACCESS_FS_READ_FILE
  • LANDLOCK_ACCESS_FS_READ_DIR
  • LANDLOCK_ACCESS_FS_REMOVE_FILE
  • LANDLOCK_ACCESS_FS_MAKE_REG

백도어를 배포하는 데 XZ를 사용한 행위 자체는 완전히 정상적이었으므로 Landlock이 공격의 어느 부분도 막지는 못했을 것이다. 그렇다면 Landlock을 제거한 이유는 무엇일까? 이 질문에는 여전히 답이 없으며 몇 가지 가설이 있다.

공격을 시작하기 직전의 ‘패닉’에 따른 결정이었을 수 있다. 필요하지 않더라도 백신을 끄는 경우와 비슷하다는 설명이다. 또는 공격자가 XZ에서 취약점을 발견했거나 새 취약점을 넣을 계획이 있었고, XZ를 통해 코드를 실행하거나 C2에 접속하려 했기 때문에 Landlock을 비활성화해야 했을 가능성도 있다.

어느 경우든 이 사례는 오픈소스 도구에 Landlock이 구현된 방식과, 보안 제품처럼 공격자의 표적이 될 수 있다는 점을 보여준다. Sekoia는 XZ에서 Landlock이 유용한 이유를 보여주기 위해 잘 알려진 LD_PRELOAD 하이재킹 기법으로 악성 라이브러리를 로드하는 실험도 제시한다.

해당 라이브러리는 XZ가 사용하는 write 함수를 다시 정의하고, XZ의 자식 프로세스로 ls /tmp 명령을 실행한다. 현실적인 방식은 아니지만, 바이너리(XZ)의 잠재적 취약점이나 버그를 모사해 Landlock이 리눅스 바이너리의 공격 표면을 줄이는 데 어떻게 쓰일 수 있는지 보여준다.

라이브러리를 컴파일한 뒤 LD_PRELOAD로 불러올 수 있다. Sekoia는 XZ 두 버전으로 시험했다. Debian Trixie에 기본 설치되는 5.8.1은 Landlock을 포함해 컴파일됐다. ./src/xz/xz에 있는 구버전 5.0.8은 Landlock 없이 컴파일됐다. Landlock을 포함해 컴파일한 XZ 바이너리는 ls /tmp 실행을 거부했지만, 구버전은 명령 실행을 허용했다. 원문은 이를 모사하는 셸 명령을 그림 6에 제시한다.

악성 실행을 차단하는 것만으로는 충분하지 않다. 공격자가 다른 방법을 찾을 수 있기 때문이다. Landlock은 차단 사실도 기록하므로 방어 담당자는 진행 중인 공격 가능성을 파악할 수 있고, Sekoia SOC 플랫폼에서 경보가 발생한다. 원문은 해당 경보를 그림 7에 제시한다.

결론

Sekoia는 Landlock이 정밀한 샌드박스와 오탐률이 낮은 행위 기반 규칙을 만들 수 있게 하는 유용한 보안 메커니즘이자 탐지 텔레메트리의 원천이라고 평가한다. XZ Utils 같은 도구에 통합되고 공격자의 표적이 된 사례는 Landlock의 역할이 커지고 있음을 보여준다고 설명한다.

Landlock은 리눅스 커널 5.13부터 통합됐고, 로깅 기능은 6.15부터 제공된다. 도입된 지 오래되지 않았지만 이미 여러 도구가 사용하고 있으며 앞으로 더 많은 도구에 적용될 수 있다. 방어 담당자는 시스템 강화뿐 아니라 SOC 경보에 활용할 로그를 확보하는 수단으로 Landlock을 검토할 수 있다. Sekoia는 방어 담당자가 다른 보안 제품을 활용하듯 Landlock을 시스템 강화와 향후 탐지 범위에 포함해야 한다고 본다. 공격을 막는 것도 중요하지만, 차단 사실을 아는 것 역시 중요하다는 설명이다.

Sekoia는 탐지 범위를 넓히고 탐지 규칙을 보완하기 위해 새로운 텔레메트리 원천을 계속 찾고 있으며, Landlock을 통해 두 목표를 모두 개선했다고 밝혔다. 앞으로 Landlock의 개선 사항과 다른 LSM도 계속 살펴볼 계획이다.

부록

취약한 Go 웹 서버

go
package mainimport ( "fmt" "io" "log" "net/http" "os" "path/filepath" "github.com/landlock-lsm/go-landlock/landlock")const allowedDir = "/var/lib/htdocs"// vulnerableHandler reads the 'file' query parameter with no validation,// then opens and serves that file by joining it with allowedDir.func vulnerableHandler(w http.ResponseWriter, r *http.Request) { reqFile := r.URL.Query().Get("file") if reqFile == "" { http.Error(w, "'file' query parameter required", http.StatusBadRequest) return } path := filepath.Join(allowedDir, reqFile) fmt.Printf("Serving file: %s\n", path) file, err := os.Open(path) if err != nil { http.Error(w, "File not found or access denied", http.StatusNotFound) return } defer file.Close() if _, err := io.Copy(w, file); err != nil { http.Error(w, "Error reading file", http.StatusInternalServerError) }}func main() { err := landlock.V5.BestEffort().RestrictPaths( landlock.RODirs(allowedDir), ) if err != nil { log.Fatalf("Failed to apply Landlock restrictions: %v", err) } http.HandleFunc("/", vulnerableHandler) fmt.Println("Vulnerable HTTP server with Landlock on :8080") log.Fatal(http.ListenAndServe(":8080", nil))}

`write` 함수를 다시 정의하는 악성 라이브러리

c
#define _GNU_SOURCE#include <stdio.h>#include <dlfcn.h>#include <unistd.h>#include <errno.h>#include <string.h>#include <stdlib.h>#include <sys/wait.h>static ssize_t (*real_write)(int fd, const void *buf, size_t count) = NULL;ssize_t write(int fd, const void *buf, size_t count) { if (!real_write) real_write = dlsym(RTLD_NEXT, "write"); static int attempted = 0; // Triggers if stdout (1) or file (>2) if (!attempted && (fd == 1 || fd > 2)) { attempted = 1; fprintf(stderr, "\nInjected - Trying to execute 'ls /tmp'...\n"); pid_t pid = fork(); if (pid == 0) { dup2(STDERR_FILENO, STDOUT_FILENO); execl("/usr/bin/ls", "ls", "/tmp", NULL); // If Landlock blocked, print error  fprintf(stderr, "(%s)\n", strerror(errno)); exit(1); } else if (pid > 0) { wait(NULL); } } return real_write(fd, buf, count); }

Sigma 탐지 규칙

yaml
version: 2.0uuid: ef477b0d-71d8-4190-a6c7-c2187f7d6ddbrule: Landlock Denied Accessdescription: >- Landlock LSM (Linux Security Module), has denied an access requests. This is logged by default for a sandboxed program if audit is enabled.sources: https://docs.kernel.org/admin-guide/LSM/landlock.htmleffort: intermediatedetection: selection: action.properties.domain : '*' action.properties.blockers: '*' condition: selectiontags: - Linuxsimilarity_strategy: - action.properties.domain