Remco SprootenRuben Groenewoud

푸마킷 해제

PUMAKIT은 고급 스텔스 메커니즘을 사용하여 자신의 존재를 숨기고 명령 및 제어 서버와의 통신을 유지하는 정교한 LKM(로드 가능한 커널 모듈) 루트킷입니다.

30분 읽기Malware 분석
PUMAKIT 발톱 제거

푸마킷 한눈에 보기

푸마킷은 정교한 멀웨어로, VirusTotal의 일상적인 위협 헌팅 중에 처음 발견되었으며 바이너리에서 발견된 개발자가 임베드한 문자열의 이름을 따서 명명되었습니다. 이 다단계 아키텍처는 드로퍼(cron), 메모리 상주 실행 파일 2개(/memfd:tgt/memfd:wpn), LKM 루트킷 모듈, 공유 객체(SO) 유저랜드 루트킷으로 구성됩니다.

악성코드 작성자가 "PUMA", 내부 Linux 함수 추적기(ftrace)를 사용하여 18 개의 다른 시스템 호출과 여러 커널 함수를 후킹하여 핵심 시스템 동작을 조작할 수 있도록 합니다. 권한 에스컬레이션을 위한 rmdir() 시스콜과 구성 및 런타임 정보 추출을 위한 특수 명령어를 사용하는 등 고유한 방법을 사용하여 PUMA와 상호 작용합니다. 단계적 배포를 통해 LKM 루트킷은 보안 부팅 검사 또는 커널 심볼 가용성과 같은 특정 조건이 충족될 때만 활성화되도록 합니다. 이러한 조건은 Linux 커널을 스캔하여 확인되며, 필요한 모든 파일은 드롭퍼 내에 ELF 바이너리로 포함되어 있습니다.

커널 모듈의 주요 기능에는 권한 에스컬레이션, 파일 및 디렉터리 숨기기, 시스템 도구로부터 숨기기, 디버깅 방지 조치, 명령 및 제어(C2) 서버와의 통신 설정 등이 있습니다.

핵심 사항

  • 다단계 아키텍처: 이 멀웨어는 드로퍼, 메모리 상주 실행 파일 2개, LKM 루트킷, SO 유저랜드 루트킷을 결합하여 특정 조건에서만 활성화됩니다.
  • 고급 스텔스 메커니즘: 18 시스템 호출과 ftrace() 을 사용하는 여러 커널 함수를 연결하여 파일, 디렉터리 및 루트킷 자체를 숨기고 디버깅 시도를 회피합니다.
  • 고유 권한 에스컬레이션: 권한 에스컬레이션 및 루트킷과의 상호 작용을 위해 rmdir() 시스템 호출과 같은 비정상적인 후킹 방법을 활용합니다.
  • 중요 기능: 권한 에스컬레이션, C2 통신, 디버깅 방지, 시스템 조작을 통해 지속성과 제어력을 유지합니다.

푸마킷 디스커버리

VirusTotal에서 일상적인 위협 헌팅을 하던 중, 저희는 cron이라는 흥미로운 바이너리를 발견했습니다. 이 바이너리는 9월 4, 2024일에 처음 업로드되었으며 0 번 탐지되어 은닉 가능성에 대한 의혹이 제기되었습니다. 추가 조사 결과, 같은 날 업로드된 또 다른 관련 아티팩트/memfd:wpn (deleted)71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24도 0 개의 탐지 항목이 있는 것으로 확인되었습니다.

저희의 관심을 끈 것은 /boot/에서 vmlinuz 커널 패키지의 잠재적 조작 가능성을 암시하는 문자열이 이 바이너리에 포함되어 있다는 점입니다. 이로 인해 샘플에 대한 심층 분석이 진행되었고, 그 결과 샘플의 행동과 목적에 대한 흥미로운 결과가 도출되었습니다.

푸마킷 코드 분석

내장된 LKM 루트킷 모듈(악성코드 제작자가 "PUMA" )과 SO 유저랜드 루트킷인 Kitsune의 이름을 딴 PUMAKIT은 실행 체인을 시작하는 드롭퍼부터 시작하여 다단계 아키텍처를 사용합니다. 이 프로세스는 cron 바이너리로 시작하여 메모리 상주 실행 파일 두 개를 만듭니다: /memfd:tgt (deleted)/memfd:wpn (deleted). /memfd:tgt 은 양성 Cron 바이너리 역할을 하는 반면, /memfd:wpn 은 루트킷 로더 역할을 합니다. 로더는 시스템 상태를 평가하고, 임시 스크립트(/tmp/script.sh)를 실행하며, 궁극적으로 LKM 루트킷을 배포하는 역할을 합니다. LKM 루트킷에는 사용자 공간에서 루트킷과 상호 작용하기 위해 내장된 SO 파일인 Kitsune이 포함되어 있습니다. 이 실행 체인은 아래에 표시되어 있습니다.

이러한 구조적 설계를 통해 PUMAKIT은 특정 기준이 충족될 때만 페이로드를 실행하여 은밀성을 보장하고 탐지 가능성을 줄일 수 있습니다. 프로세스의 각 단계는 메모리 상주 파일과 대상 환경에 대한 정밀한 검사를 활용하여 그 존재를 숨기도록 세심하게 제작되었습니다.

이 섹션에서는 여러 단계에 대한 코드 분석을 자세히 살펴보고, 이 정교한 다단계 멀웨어를 가능하게 하는 구성 요소와 그 역할을 살펴봅니다.

1단계: Cron 개요

cron 바이너리는 드롭퍼 역할을 합니다. 아래 함수는 PUMAKIT 멀웨어 샘플에서 메인 로직 핸들러 역할을 합니다. 주요 목표는 다음과 같습니다:

  1. 특정 키워드("Huinder")에 대한 명령줄 인수를 확인합니다.
  2. 찾을 수 없는 경우 숨겨진 페이로드를 파일시스템에 드롭하지 않고 메모리에서 완전히 임베드하여 실행합니다.
  3. 발견되면 특정 "추출" 인수를 처리하여 내장된 구성 요소를 디스크에 덤프한 다음 우아하게 종료합니다.

간단히 말해, 멀웨어는 은신 상태를 유지하려고 합니다. 특정 인수 없이 정상적으로 실행하면 디스크에 흔적을 남기지 않고 숨겨진 ELF 바이너리를 실행하며, cron과 같이 정상적인 프로세스로 가장할 수 있습니다.

인수에 Huinder 문자열을 찾을 수 없으면 if (!argv_) 안에 있는 코드가 실행됩니다:

writeToMemfd(...): 파일리스 실행의 특징입니다. memfd_create 은 바이너리가 메모리에만 존재하도록 허용합니다. 이 악성 코드는 내장된 페이로드(tgtElfpwpnElfp)를 디스크에 드롭하지 않고 익명의 파일 설명자에 씁니다.

fork()execveat(): 악성 코드가 하위 및 상위 프로세스로 포크됩니다. 자식은 로그를 남기지 않도록 표준 출력 및 오류를 /dev/null 로 리디렉션한 다음 execveat()을 사용하여 "무기" 페이로드(wpnElfp)를 실행합니다. 부모는 자식을 기다린 다음 "대상" 페이로드(tgtElfp)를 실행합니다. 두 페이로드 모두 디스크의 파일이 아닌 메모리에서 실행되므로 탐지 및 포렌식 분석이 더 어렵습니다.

execveat() 의 선택은 흥미롭습니다. 파일 설명자가 참조하는 프로그램을 실행할 수 있는 새로운 시스템 호출입니다. 이는 이 멀웨어의 파일리스 실행 특성을 더욱 뒷받침합니다.

tgt 파일이 합법적인 cron 바이너리임을 확인했습니다. 메모리에 로드되고 루트킷 로더(wpn)가 실행된 후 실행됩니다.

실행 후 바이너리는 호스트에서 활성 상태로 유지됩니다.

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

다음은 이 프로세스에 대한 파일 설명자 목록입니다. 이러한 파일 설명자는 드롭퍼가 생성한 메모리 상주 파일을 표시합니다.

root@debian11-rg:/tmp# ls -lah /proc/2138/fd
total 0
dr-x------ 2 root root  0 Dec  6 09:57 .
dr-xr-xr-x 9 root root  0 Dec  6 09:57 ..
lr-x------ 1 root root 64 Dec  6 09:57 0 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 1 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 2 -> /dev/null
lrwx------ 1 root root 64 Dec  6 09:57 3 -> '/memfd:tgt (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 4 -> '/memfd:wpn (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 5 -> /run/crond.pid
lrwx------ 1 root root 64 Dec  6 09:57 6 -> 'socket:[20433]'

참조를 따라가면 샘플에 로드된 바이너리를 확인할 수 있습니다. 오프셋과 크기를 사용하여 추가 분석을 위해 바이트를 새 파일에 복사하기만 하면 됩니다.

압축을 풀면 다음과 같은 두 개의 새 파일이 생깁니다:

  • Wpn: cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
  • Tgt: 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136

이제 두 메모리 파일의 덤프가 생겼습니다.

2단계: 메모리 상주 실행 파일 개요

memfd:tgt ELF 파일을 살펴보면 이것이 기본 Ubuntu Linux Cron 바이너리임을 알 수 있습니다. 바이너리에는 수정 사항이 없는 것으로 보입니다.

LKM 루트킷을 로딩하는 바이너리 파일이기 때문에 /memfd:wpn 파일이 더 흥미롭습니다. 이 루트킷 로더는 /usr/sbin/sshd 실행 파일로 모방하여 자신을 숨기려고 시도합니다. 보안 부팅이 활성화되어 있는지, 필요한 심볼을 사용할 수 있는지 등 특정 전제 조건을 확인하고 모든 조건이 충족되면 커널 모듈 루트킷을 로드합니다.

Kibana에서 실행을 살펴보면, 프로그램이 dmesg을 쿼리하여 보안 부팅이 활성화되어 있는지 여부를 확인하는 것을 볼 수 있습니다. 올바른 조건이 충족되면 script.sh 이라는 셸 스크립트가 /tmp 디렉터리에 드롭되어 실행됩니다.

이 스크립트에는 압축 형식에 따라 파일을 검사하고 처리하는 로직이 포함되어 있습니다.

기능은 다음과 같습니다:

  • 함수 c()file 명령을 사용하여 파일을 검사하여 ELF 바이너리인지 확인합니다. 그렇지 않은 경우 함수는 오류를 반환합니다.
  • 함수 d() 은 지원되는 압축 형식의 서명을 기반으로 gunzip, unxz, bunzip2 등과 같은 다양한 유틸리티를 사용하여 지정된 파일의 압축 해제를 시도합니다. greptail 를 사용하여 특정 압축 세그먼트를 찾아 추출합니다.
  • 스크립트에서 파일($i)을 /tmp/vmlinux로 찾아 처리하려고 합니다.

/tmp/script.sh을 실행한 후 /boot/vmlinuz-5.10.0-33-cloud-amd64 파일을 입력으로 사용합니다. tr 명령은 gzip의 매직넘버(\037\213\010)를 찾는 데 사용됩니다. 그 후, 바이트 오프셋 +10957311 에서 시작하는 파일의 일부가 tail를 사용하여 추출되고 gunzip으로 압축 해제된 후 /tmp/vmlinux로 저장됩니다. 그런 다음 결과 파일을 확인하여 유효한 ELF 바이너리인지 확인합니다.

이 시퀀스는 스크립트 내의 모든 항목이 d() 함수에 전달될 때까지 여러 번 반복됩니다.

d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd

이 프로세스는 아래와 같습니다.

스크립트의 모든 항목을 실행한 후 /tmp/vmlinux/tmp/script.sh 파일이 삭제됩니다.

스크립트의 주요 목적은 특정 조건이 충족되는지 확인하고 충족되는 경우 커널 객체 파일을 사용하여 루트킷을 배포하기 위한 환경을 설정하는 것입니다.

위 이미지에서 볼 수 있듯이 로더는 Linux 커널 파일에서 __ksymtab__kcrctab 기호를 찾아 오프셋을 저장합니다.

여러 문자열을 보면 루트킷 개발자가 자신의 루트킷을 '드롭퍼 내에서 PUMA" '라고 지칭하는 것을 알 수 있습니다. 조건에 따라 프로그램은 다음과 같은 메시지를 출력합니다:

PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded

또한 커널 개체 파일에 .puma-config이라는 섹션이 포함되어 있어 루트킷과의 연관성을 강화합니다.

3단계: LKM 루트킷 개요

이 섹션에서는 커널 모듈의 기본 기능을 이해하기 위해 커널 모듈을 자세히 살펴봅니다. 구체적으로 심볼 조회 기능, 후킹 메커니즘, 목표를 달성하기 위해 수정하는 주요 시스템 호출에 대해 살펴봅니다.

LKM 루트킷 개요: 심볼 조회 및 후킹 메커니즘

시스템 동작을 조작하는 LKM 루트킷의 기능은 syscall 테이블을 사용하고 심볼 확인을 위해 kallsyms_lookup_name()에 의존하는 것으로 시작됩니다. 커널 버전 5.7 이상을 대상으로 하는 최신 루트킷과 달리, 이 루트킷은 kprobes을 사용하지 않으므로 이전 커널용으로 설계되었음을 나타냅니다.

커널 버전 5.7 이전에는 kallsyms_lookup_name() 을 내보내 적절한 라이선스가 없는 모듈에서도 쉽게 활용할 수 있었기 때문에 이 선택이 중요했습니다.

2020년 2월, 커널 개발자들은 무단 또는 악의적인 모듈의 오용을 방지하기 위해 kallsyms_lookup_name() 의 내보내기 해제에 대해 논의했습니다. 일반적인 수법에는 라이선스 검사를 우회하기 위해 가짜 MODULE_LICENSE("GPL") 선언을 추가하여 이러한 모듈이 내보내지 않은 커널 함수에 액세스할 수 있도록 하는 것이 포함됩니다. LKM 루트킷은 문자열에서 알 수 있듯이 이러한 동작을 보여줍니다:

name=audit
license=GPL

이 GPL 라이선스의 부정 사용은 루트킷이 kallsyms_lookup_name() 을 호출하여 함수 주소를 확인하고 커널 내부를 조작할 수 있도록 합니다.

심볼 해상도 전략 외에도 커널 모듈은 ftrace() 후킹 메커니즘을 사용하여 후크를 설정합니다. ftrace()을 활용하여 루트킷은 효과적으로 시스템 호출을 가로채고 해당 핸들러를 사용자 지정 훅으로 대체합니다.

예를 들어 위의 코드 스니펫에 표시된 unregister_ftrace_functionftrace_set_filter_ip 의 사용은 이를 증명합니다.

LKM 루트킷 개요: 후킹된 시스콜 개요

저희는 루트킷의 시스템 호출 후킹 메커니즘을 분석하여 PUMA가 시스템 기능에 미치는 간섭 범위를 파악했습니다. 다음 표에는 루트킷에 의해 후킹된 시스템 호출, 해당 후킹된 함수 및 잠재적 목적이 요약되어 있습니다.

cleanup_module() 함수를 보면 unregister_ftrace_function() 함수를 사용하여 ftrace() 후킹 메커니즘이 되돌려지는 것을 볼 수 있습니다. 이렇게 하면 콜백이 더 이상 호출되지 않습니다. 그 후 모든 시스콜은 후킹된 시스콜이 아닌 원래 시스콜을 가리키도록 반환됩니다. 이렇게 하면 연결된 모든 시스템 호출에 대한 깔끔한 개요를 확인할 수 있습니다.

다음 섹션에서는 몇 가지 후킹된 시스콜에 대해 자세히 살펴보겠습니다.

LKM 루트킷 개요: rmdir_hook()

커널 모듈의 rmdir_hook() 은 루트킷의 기능에서 중요한 역할을 하며, 은폐 및 제어를 위해 디렉터리 제거 작업을 조작할 수 있도록 합니다. 이 훅은 단순히 rmdir() 시스템 호출을 가로채는 것에 국한되지 않고 권한 에스컬레이션을 적용하고 특정 디렉터리에 저장된 구성 세부 정보를 검색하도록 기능을 확장합니다.

이 후크에는 몇 가지 점검 사항이 있습니다. 후크는 rmdir() 시스템 호출의 첫 번째 문자가 zarya일 것으로 예상합니다. 이 조건이 충족되면 후킹된 함수는 실행되는 명령어인 6번째 문자를 확인합니다. 마지막으로 실행 중인 명령에 대한 프로세스 인수를 포함할 수 있는 8번째 문자를 확인합니다. 구조는 다음과 같습니다: zarya[char][command][char][argument]. zarya 과 명령 및 인수 사이에 특수 문자(또는 아무 문자도 포함하지 않음)를 넣을 수 있습니다.

발행일을 기준으로 다음과 같은 명령어를 확인했습니다:

명령목적
zarya.c.0Retrieve the config
zarya.t.0작업 테스트
zarya.k.<pid>PID 숨기기
zarya.v.0실행 중인 버전 받기

루트킷이 초기화되면 rmdir() 시스템 호출 후크가 루트킷이 성공적으로 로드되었는지 확인하는 데 사용됩니다. t 명령을 호출하여 이 작업을 수행합니다.

ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$

존재하지 않는 디렉터리에서 rmdir 명령을 사용하면 "해당 파일 또는 디렉터리 없음" 오류 메시지가 반환됩니다. zarya.t에서 rmdir 을 사용하는 경우 커널 모듈이 성공적으로 로드되었음을 나타내는 출력이 반환되지 않습니다.

두 번째 명령은 v으로, 실행 중인 루트킷의 버전을 가져오는 데 사용됩니다.

ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory

"'directory'을 제거하지 못했습니다." 오류에 zarya.v 이 추가되는 대신 루트킷 버전 240513 이 반환됩니다.

세 번째 명령은 c으로, 루트킷의 구성을 출력합니다.

ubuntu-rk:~/testing$ ./dump_config "zarya.c"
rmdir: failed to remove '': No such file or directory
Buffer contents (hex dump):
7ffe9ae3a270  00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76  .....ping_interv
7ffe9ae3a280  61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f  al_s.,....sessio
7ffe9ae3a290  6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00  n_timeout_s.....
7ffe9ae3a2a0  10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8  .c2_timeout_s...
7ffe9ae3a2b0  00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72  ...tag.....gener
7ffe9ae3a2c0  69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65  ic..s_a0.....rhe
7ffe9ae3a2d0  6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72  l.opsecurity1.ar
7ffe9ae3a2e0  74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33  t..s_p0.....8443
7ffe9ae3a2f0  00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02  ..s_c0.....tls..
7ffe9ae3a300  73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73  s_a1.....sec.ops
7ffe9ae3a310  65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f  ecurity1.art..s_
7ffe9ae3a320  70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63  p1.....8443..s_c
7ffe9ae3a330  31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00  1.....tls..s_a2.
7ffe9ae3a340  0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30  ....89.23.113.20
7ffe9ae3a350  34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33  4..s_p2.....8443
7ffe9ae3a360  00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00  ..s_c2.....tls..

페이로드가 널 바이트로 시작하므로 rmdir 셸 명령을 통해 zarya.c 을 실행할 때 출력은 반환되지 않습니다. 시스템 호출을 래핑하고 16진수/ASCII 표현을 출력하는 작은 C 프로그램을 작성하면 반환되는 루트킷의 구성을 확인할 수 있습니다.

대부분의 루트킷이 루트 권한을 얻기 위해 kill() 시스템 호출을 사용하는 대신, 이 루트킷은 이 목적으로도 rmdir() 시스템 호출을 활용합니다. 루트킷은 prepare_creds 함수를 사용하여 자격 증명 관련 ID를 0 (root)로 수정하고, 이 수정된 구조에서 commit_creds 를 호출하여 현재 프로세스 내에서 루트 권한을 획득합니다.

이 함수를 트리거하려면 6번째 문자를 0으로 설정해야 합니다. 이 훅의 주의할 점은 호출자 프로세스에게 루트 권한을 부여하지만 유지하지는 않는다는 것입니다. zarya.0을 실행하면 아무 일도 일어나지 않습니다. 그러나 C 프로그램에서 이 훅을 호출하고 현재 프로세스의 권한을 출력하면 결과가 나옵니다. 사용되는 래퍼 코드의 스니펫이 아래에 표시됩니다:

[...]
// Print the current PID, SID, and GID
pid_t pid = getpid();
pid_t sid = getsid(0);  // Passing 0 gets the SID of the calling process
gid_t gid = getgid();

printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid);

// Print all credential-related IDs
uid_t ruid = getuid();    // Real user ID
uid_t euid = geteuid();   // Effective user ID
gid_t rgid = getgid();    // Real group ID
gid_t egid = getegid();   // Effective group ID
uid_t fsuid = setfsuid(-1);  // Filesystem user ID
gid_t fsgid = setfsgid(-1);  // Filesystem group ID

printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n",
    ruid, euid, rgid, egid, fsuid, fsgid);

[...]

함수를 실행하면 다음과 같은 출력을 얻을 수 있습니다:

ubuntu-rk:~/testing$ whoami;id
ruben
uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)

ubuntu-rk:~/testing$ ./rmdir zarya.0
Received data:
zarya.0
Current PID: 41838, SID: 35117, GID: 0
Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0

이 훅을 활용하기 위해 rmdir zarya.0 명령을 실행하고 이제 /etc/shadow 파일에 액세스할 수 있는지 확인하는 작은 C 래퍼 스크립트를 작성했습니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main() {
    const char *directory = "zarya.0";

    // Attempt to remove the directory
    if (syscall(SYS_rmdir, directory) == -1) {
        fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno));
    } else {
        printf("rmdir: successfully removed '%s'\n", directory);
    }

    // Execute the `id` command
    printf("\n--- Running 'id' command ---\n");
    if (system("id") == -1) {
        perror("Failed to execute 'id'");
        return 1;
    }

    // Display the contents of /etc/shadow
    printf("\n--- Displaying '/etc/shadow' ---\n");
    if (system("cat /etc/shadow") == -1) {
        perror("Failed to execute 'cat /etc/shadow'");
        return 1;
    }

    return 0;
}

성공했습니다.

ubuntu-rk:~/testing$ ./get_root
rmdir: successfully removed 'zarya.0'

--- Running 'id' command ---
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)

--- Displaying '/etc/shadow' ---
root:*:19430:0:99999:7:::
[...]

rmdir() 함수에서 사용할 수 있는 명령이 더 있지만 지금은 다음 단계로 넘어가고 향후 발행물에 추가할 수 있습니다.

LKM 루트킷 개요: getdents() 및 getdents64() 후크

루트킷의 getdents_hook()getdents64_hook() 은 디렉터리 목록 시스템 호출을 조작하여 파일과 디렉터리를 사용자에게 숨기는 역할을 합니다.

getdents() 및 getdents64() 시스템 호출은 디렉토리 항목을 읽는 데 사용됩니다. 루트킷은 이러한 함수를 연결하여 특정 조건과 일치하는 모든 항목을 필터링합니다. 특히 접두사가 zov_인 파일 및 디렉토리는 디렉토리의 콘텐츠를 나열하려는 모든 사용자로부터 숨겨집니다.

예를 들면, 다음과 같습니다.

ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir

ubuntu-rk:~/getdents_hook$ ls -lah
total 8.0K
drwxrwxr-x  3 ruben ruben 4.0K Dec  9 11:11 .
drwxr-xr-x 11 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file

ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/
total 8.0K
drwxrwxr-x 2 ruben ruben 4.0K Dec  9 11:11 .
drwxrwxr-x 3 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file
this file is now hidden

여기서 zov_hidden 파일은 전체 경로를 사용하여 직접 액세스할 수 있습니다. 그러나 ls 명령을 실행하면 디렉터리 목록에 나타나지 않습니다.

4단계: 키츠네 SO 개요

루트킷을 더 자세히 조사하는 동안 커널 객체 파일 내에서 또 다른 ELF 파일이 발견되었습니다. 이 바이너리를 추출한 후 /lib64/libs.so 파일임을 확인했습니다. 조사 결과 Kitsune PID %ld과 같은 문자열에 대한 참조가 여러 개 발견되었습니다. 이는 개발자가 SO를 키츠네라고 부르는 것을 암시합니다. 루트킷에서 관찰되는 특정 동작에 대한 책임은 Kitsune에게 있을 수 있습니다. 이러한 참조는 루트킷이 LD_PRELOAD를 통해 사용자 공간 상호작용을 조작하는 방법에 대한 광범위한 맥락과 일치합니다.

이 SO 파일은 이 루트킷의 핵심인 지속성과 은폐 메커니즘을 달성하는 데 중요한 역할을 하며, 공격 체인 내에 통합되어 있다는 것은 이 루트킷의 설계가 정교함을 보여줍니다. 이제 공격 체인의 각 부분을 탐지 및/또는 방지하는 방법을 소개합니다.

PUMAKIT 실행 체인 탐지 & 예방

이 섹션에서는 PUMAKIT 실행 체인의 여러 부분을 방지하고 탐지할 수 있는 다양한 EQL/KQL 규칙과 YARA 서명을 표시합니다.

1단계: 크론

드로퍼가 실행되면 흔하지 않은 이벤트가 syslog에 저장됩니다. 이 이벤트는 프로세스가 실행 스택으로 시작되었음을 나타냅니다. 이는 흔치 않은 일이지만 흥미롭게 지켜볼 수 있습니다:

[  687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack

다음 쿼리를 통해 검색할 수 있습니다:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"

이 메시지는 /var/log/messages 또는 /var/log/syslog에 저장됩니다. Filebeat 또는 Elastic 에이전트 시스템 통합을 통해 시스템 로그를 읽음으로써 이를 감지할 수 있습니다.

2단계: 메모리 상주 실행 파일

비정상적인 파일 디스크립터 실행을 바로 확인할 수 있습니다. 이는 다음 EQL 쿼리를 통해 감지할 수 있습니다:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init"

이 파일 설명자는 프로세스가 종료될 때까지 드롭퍼의 부모로 유지되므로 이 부모 프로세스를 통해 여러 파일도 실행됩니다:

file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like (
  "/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*"
  "/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*"
)

/tmp/script.sh 이 삭제된 후(위의 쿼리를 통해 감지됨) 파일 속성 검색 및 보관 취소 활동을 쿼리하여 실행을 감지할 수 있습니다:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and 
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or
  (process.name == "grep" and process.args == "ELF") or
  (process.name in ("lzop", "lz4") and process.args in ("-d", "--decode"))
) and
not process.parent.name == "mkinitramfs"

스크립트는 tail 명령을 통해 Linux 커널 이미지의 메모리를 계속 찾습니다. 이는 다른 메모리 탐색 도구와 함께 다음 쿼리를 통해 감지할 수 있습니다:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or
  (process.name == "cmp" and process.args == "-i") or
  (process.name in ("hexdump", "xxd") and process.args == "-s") or
  (process.name == "dd" and process.args : ("skip*", "seek*"))
)

/tmp/script.sh 실행이 완료되면 /memfd:tgt (deleted)/memfd:wpn (deleted) 가 만들어집니다. tgt 실행 파일은 /run/crond.pid 파일을 생성하며, 이는 정상 Cron 실행 파일입니다. 이는 악의적인 것이 아니라 간단한 쿼리를 통해 탐지할 수 있는 아티팩트입니다.

file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and
file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null

wpn 실행 파일은 모든 조건이 충족되면 LKMrootkit을 로드합니다.

3단계: 루트킷 커널 모듈

커널 모듈 로딩은 다음 구성을 적용하여 Auditd 매니저를 통해 감지할 수 있습니다:

-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules

그리고 다음 쿼리를 사용합니다:

driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")

Linux 탐지 엔지니어링 환경을 개선하기 위해 Auditd를 Elastic Security와 함께 활용하는 방법에 대한 자세한 내용은 Elastic Security Labs 사이트에 게시된 Auditd를 사용한 Linux 탐지 엔지니어링 연구를 참조하세요.

초기화 시 LKM은 커널이 서명되지 않았기 때문에 커널을 오염시킵니다.

audit: module verification failed: signature and/or required key missing - tainting kernel

다음 KQL 쿼리를 통해 이 동작을 감지할 수 있습니다:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel"

또한 LKM에 결함이 있는 코드가 있어 여러 번 세그폴트를 일으킵니다. 예를 들어

Dec  9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4
Dec  9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89

이는 kern.log 파일에서 세그 오류를 쿼리하는 간단한 KQL 쿼리를 통해 감지할 수 있습니다.

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault

커널 모듈이 로드되면 kthreadd 프로세스를 통해 명령 실행의 흔적을 볼 수 있습니다. 루트킷은 특정 명령을 실행하기 위해 새로운 커널 스레드를 생성합니다. 예를 들어, 루트킷은 짧은 간격으로 다음 명령을 실행합니다:

cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest

다음과 같은 쿼리를 통해 이러한 명령어 및 기타 잠재적으로 의심스러운 명령을 탐지할 수 있습니다:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and (
  process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or
  process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or
  process.command_line like (
    "*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*",
    "*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*",
    "*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*",
    "*xxd *", "*/etc/shadow*"
  )
) and not process.name == "dpkg"

또한 rmdir 명령의 비정상적인 UID/GID 변경을 분석하여 루트킷의 권한 상승 방법을 탐지할 수도 있습니다.

process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"

실행 체인에 따라 다른 여러 동작 규칙도 트리거될 수 있습니다.

모든 것을 지배하는 하나의 YARA 서명

Elastic Security는 PUMAKIT(드로퍼(cron), 루트킷 로더(/memfd:wpn), LKM 루트킷 및 Kitsune 공유 개체 파일)을 식별하기 위해 YARA 서명을 생성했습니다. 서명은 아래에 표시됩니다:

rule Linux_Trojan_Pumakit {
    meta:
        author = "Elastic Security"
        creation_date = "2024-12-09"
        last_modified = "2024-12-09"
        os = "Linux"
        arch = "x86, arm64"
        threat_name = "Linux.Trojan.Pumakit"

    strings:
        $str1 = "PUMA %s"
        $str2 = "Kitsune PID %ld"
        $str3 = "/usr/share/zov_f"
        $str4 = "zarya"
        $str5 = ".puma-config"
        $str6 = "ping_interval_s"
        $str7 = "session_timeout_s"
        $str8 = "c2_timeout_s"
        $str9 = "LD_PRELOAD=/lib64/libs.so"
        $str10 = "kit_so_len"
        $str11 = "opsecurity1.art"
        $str12 = "89.23.113.204"
    
    condition:
        4 of them
}

관찰

이 연구에서는 다음과 같은 관찰 가능성에 대해 논의했습니다.

관측 가능합니다.유형이름참조
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cron푸마킷 드롭퍼
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)푸마킷 로더
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)크론 바이너리
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.so키츠네 공유 객체 참조
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elf푸마킷 변형
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.so키츠네 공유 객체 변형
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soKitsune
sec.opsecurity1[.]art도메인 이름PUMAKIT C2 서버
rhel.opsecurity1[.]art도메인 이름PUMAKIT C2 서버
89.23.113[.]204IPv4-addrPUMAKIT C2 서버

결론

푸마킷은 시스콜 후킹, 메모리 상주 실행, 고유한 권한 상승 방법과 같은 고급 기술을 사용하는 복잡하고 은밀한 위협입니다. 다중 아키텍처 설계는 Linux 시스템을 노리는 멀웨어가 점점 더 정교해지고 있다는 점을 강조합니다.

Elastic Security Labs는 계속해서 PUMAKIT을 분석하고, 그 동작을 모니터링하며, 업데이트나 새로운 변종을 추적할 것입니다. 탐지 방법을 개선하고 실행 가능한 인사이트를 공유함으로써 방어자가 한 발 앞서 나갈 수 있도록 지원합니다.