BPFDoor 분석

최근 SKT 해킹 사건으로 전국이 난리다. 이 사건에서 사용된 멀웨어가 어떤 것인지 정확히 알 수는 없지만, 언론에서는 여러 소식을 근거로 해킹 사건에 BPFDoor 로 알려진 멀웨어가 사용되었을 것이라 추측하고 있다.

멀웨어 분석가로서 이런 이슈를 놓칠 수 없기에 분석해보기로 결정했다.
1. 샘플 수집
MalwareBazaar 에서 Signature 값이 BPFBoor 인 파일을 검색한 결과 중 하나를 선택하여 분석했다. 참고로 이 파일은 실제 SKT 해킹 사건과는 아무런 상관이 없다.

수집한 파일의 정보는 다음과 같다.
- MD5: 156226c90974180cc4b5f9738e80f1f8
- SHA-1: ed0cd45c3bb95ef8da214048799395e247040d17
- SHA-256: 4c5cf8f977fc7c368a8e095700a44be36c8332462c0b1e41bff03238b2bf2a2d
- 파일 크기: 27712 바이트
- 파일 타입: ELF
2. 악성 행위 분석
2.1 중복 실행 방지
이 멀웨어는 초기에 /var/run/auditd.lock
의 존재 여부를 확인한 후, 해당 파일이 존재하면 악성 행위 없이 바로 종료된다.

최초 실행 시 /var/run/auditd.lock
파일을 생성하며, 앞서 설명한 기능과 연계되어 멀웨어가 여러 번 중복 실행되는 것을 방지한다.

또한 해당 파일은 프로세스 정상 종료 시 발생하는 SIGTERN 시그널를 처리하는 핸들러 코드를 등록하는데, 이 코드는 /var/run/auditd.lock
를 삭제하는 기능을 가진다. 따라서 BPFDoor 가 종료될 시, /var/run/auditd.lock
파일은 삭제된다.
2.2 --init
인자 재실행
BPFDoor 는 관리자 권한으로 실행 시 /var/lock/
경로에 kdumpflush
라는 이름으로 자기 자신을 복제한다. kdumpflush
라는 파일은 일반적으로 linux 에서 사용되는 이름은 아니지만, 리눅스 커널에서는 kdump
라는 이름의 정상적인 파일을 사용한다. BPFDoor 는 해당 파일과 유사한 이름으로 자기 자신을 복제한 후, --init
인자와 함께 실행된다.

2.3 프로세스 은닉
BPFDoor 는 prctl()
함수에 PR_SET_NAME
(0xf) 인자와 프로세스 이름을 전달해 실행 중인 자기 자신 프로세스의 이름을 조작한다.

무작위로 선정되는 정상 프로세스 이름은 아래와 같이 하드코딩되어 있다.
/sbin/udevd -d
/sbin/mingetty /dev/tty6
/usr/sbin/console-kit-daemon --no-daemon
hald-addon-acpi: listening on acpi kernel interface /proc/acpi/event
dbus-daemon --system
hald-runner
pickup -l -t fifo -u
avahi-daemon: chroot helper
/sbin/auditd -n
/usr/lib/systemd/systemd-journald
실제로 실행해보니 avahi-daemon: chroot helper
라는 이름의 프로세스로 변경된 것을 확인할 수 있었다.

2.4 Berkeley packet filters 세팅
BPFDoor 는 리눅스 시스템에서 조건에 맞는 패킷만을 필터링하여 처리할 수 있는 Berkeley packet filters 라는 기능을 이용한다. 해당 샘플은 통신을 위한 소켓을 생성할 때 30개의 바이너리 필터 명령어를 실행해 조건에 맞는 패킷만을 필터링한다.

BPF 에 명시된 30개의 바이너리 명령어를 해석하면 아래와 같다.
번호 | Hex Bytes (code jt jf k) | 해석된 BPF 명령어 | 주석 |
---|---|---|---|
000 | 28 00 00 00 0c 00 00 00 |
ldh [12] |
A = 프레임 오프셋 12에서 2바이트 로드 (Ethernet Type) |
001 | 15 00 00 1b 00 08 00 00 |
jeq #0x800 jt 2 jf 29 |
if (A == 0x0800 (IPv4)) goto 2 else goto 29 (REJECT) |
002 | 30 00 00 00 17 00 00 00 |
ldb [23] |
A = 프레임 오프셋 23에서 1바이트 로드 (IP Protocol) |
003 | 15 00 00 05 11 00 00 00 |
jeq #0x11 jt 4 jf 9 |
if (A == 0x11 (UDP)) goto 4 else goto 9 |
004 | 28 00 00 00 14 00 00 00 |
ldh [20] |
A = 프레임 오프셋 20에서 2바이트 로드 (IP Flags & Fragment Offset) |
005 | 45 00 17 00 FF 1F 00 00 |
jset #0x1fff jt 6 jf 29 |
if (A & 0x1FFF != 0) (단편화된 후속 조각) goto 6 else goto 29 (REJECT) |
006 | B1 00 00 00 0E 00 00 00 |
ldxb 4*([14]&0xf) |
X = IP 헤더 길이 (프레임 오프셋 14는 IP 헤더 시작) |
007 | 48 00 00 00 16 00 00 00 |
ldh [x + 22] |
A = UDP 페이로드 내 특정 값 로드 (IP헤더시작(14) + X + 22) |
008 | 15 00 13 14 55 72 00 00 |
jeq #0x7255 jt 28 jf 29 |
if (A == 0x7255) goto 28 (ACCEPT) else goto 29 (REJECT) |
009 | 15 00 00 07 01 00 00 00 |
jeq #0x1 jt 10 jf 17 |
(UDP 아니면) if (A == 0x01 (ICMP)) goto 10 else goto 17 |
010 | 28 00 00 00 14 00 00 00 |
ldh [20] |
A = IP Flags & Fragment Offset |
011 | 45 00 11 00 FF 1f 00 00 |
jset #0x1fff jt 12 jf 29 |
if (A & 0x1FFF != 0) (단편화된 후속 조각) goto 12 else goto 29 (REJECT) |
012 | B1 00 00 00 0E 00 00 00 |
ldxb 4*([14]&0xf) |
X = IP 헤더 길이 |
013 | 48 00 00 00 16 00 00 00 |
ldh [x + 22] |
A = ICMP 페이로드 내 특정 값 로드 (IP헤더시작(14) + X + 22) |
014 | 15 00 00 0E 55 72 00 00 |
jeq #0x7255 jt 15 jf 16 |
if (A == 0x7255) goto 15 (추가 ICMP 검사 가능성) else goto 16 (다음 조건 또는 REJECT) |
015 | 50 00 00 00 0E 00 00 00 |
ldb [14] |
A = IP 헤더 첫 바이트 (Version & IHL) (프레임 오프셋 14) |
016 | 15 00 0B 0C 08 00 00 00 |
jeq #0x8 jt 28 jf 29 |
if (A == 0x08 (ICMP Echo Req, 이전 A값 덮어쓰임 - 로직 오류 가능성)) goto 28 (ACCEPT) else goto 29 (REJECT) |
017 | 15 00 00 0B 06 00 00 00 |
jeq #0x6 jt 18 jf 29 |
(ICMP 아니면) if (A == 0x06 (TCP)) goto 18 else goto 29 (REJECT) |
018 | 28 00 00 00 14 00 00 00 |
ldh [20] |
A = IP Flags & Fragment Offset |
019 | 45 00 09 00 FF 1F 00 00 |
jset #0x1fff jt 20 jf 29 |
if (A & 0x1FFF != 0) (단편화된 후속 조각) goto 20 else goto 29 (REJECT) |
020 | B1 00 00 00 0E 00 00 00 |
ldxb 4*([14]&0xf) |
X = IP 헤더 길이 |
021 | 50 00 00 00 1A 00 00 00 |
ldb [x + 26] |
A = TCP 페이로드 내 특정 바이트 로드 (IP헤더시작(14) + X + 26) |
022 | 54 00 00 00 F0 00 00 00 |
and #0xf0 |
A = A & 0xF0 (상위 4비트 마스크) |
023 | 74 00 00 00 02 00 00 00 |
lsh #2 |
A = A << 2 (왼쪽으로 2비트 시프트) |
024 | 0C 00 00 00 00 00 00 00 |
add x |
A = A + X (계산된 값 + IP 헤더 길이) -> X' (동적 오프셋 계산) |
025 | 07 00 00 00 00 00 00 00 |
tax |
X = A (계산된 오프셋을 X 레지스터에 저장) |
026 | 48 00 00 00 0E 00 00 00 |
ldh [x + 14] |
A = TCP 페이로드 내 동적 오프셋(X') + 14 위치에서 2바이트 로드 |
027 | 15 00 00 01 93 52 00 00 |
jeq #0x5293 jt 28 jf 29 |
if (A == 0x5293 (TCP 매직 넘버)) goto 28 (ACCEPT) else goto 29 (REJECT) |
028 | 06 00 00 00 FF FF 00 00 |
ret #0xffff |
ACCEPT packet (패킷 전체를 통과시킴) |
029 | 06 00 00 00 00 00 00 00 |
ret #0 |
REJECT packet (패킷을 버림) |
위 명령어를 요약하면 아래와 같다.
- IPv4 만 허용.
- TCP,UDP,ICMP 인 경우만 허용.
- UDP,ICMP 인 경우 패킷 페이로드 + 22 오프셋 위치의 2바이트 값이 0x7255 인지 확인.
- TCP 인 경우 패킷 페이로드 + 14 오프셋 위치의 2바이트 값이 0x5293인지 확인.
실제로 BPFDoor 를 실행 후, ss -0pb
명령어를 실행하면 네트워크 인터페이스에서 필터링하는 조건을 확인할 수 있다.

실행된 결과를 자세히 보면 UDP,ICMP 의 비교 값 29269(0x7255) 와 TCP 의 비교 값 21139(0x5293) 이 출력되는 것을 볼 수 있다.

위와 같이 특정한 패킷만을 받도록 세팅한 후, BPFDoor 는 공격자로부터 명령이 오기만을 기다린다.
2.5 CC 명령 실행
BPFDoor 의 주된 악성 행위는 공격자가 운영하는 CC로부터 패킷을 주고 받고 명령을 수행하는 것이며, 이는 최초 프로세스가 생성한 자식 프로세스 /usr/libexec/postfix/master
가 담당한다.

자식 프로세스는 공격자로부터 전송되는 패킷을 해석한 후, 아래와 같은 악성 행위를 수행한다.
2.5.1 Port knocker
CC 명령어의 기능 중 하나는 패킷에서 지정한 IP, Port 번호에 대한 접속을 허용하는 룰을 생성하는 것이다. 해당 기능의 함수에는 아래와 같은 iptables 룰을 생성하는 문자열이 하드코딩되어 있다.
"/sbin/iptables -I INPUT -p tcp -s %s -j ACCEPT"
"/sbin/iptables -D INPUT -p tcp -s %s -j ACCEPT"
"/sbin/iptables -t nat -A PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d"
"/sbin/iptables -t nat -D PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d"
CC 명령 패킷의 IP, Port 번호가 지정되면 system()
함수로 이를 실행해 iptables 룰을 생성한다.

2.5.2 UPD 패킷 송신
CC 명령어 기능 중 하나는 지정한 IP, Port 로 고정된 1바이트 UDP 패킷을 전송하는 것이다. 이 기능은 어떤 용도로 사용되는지 불분명하나, 반복적으로 빠르게 패킷을 보낸다면 UDP Flooding 기능으로 사용될 수도 있다.

2.5.3 Reverse Shell 수립
마지막 기능은 공격자가 지정한 명령어를 실행하는, 즉 리버스 셸을 수립하는 기능이다.
공격자가 전달한 임의의 명령어를 실행하며, 실행된 결과를 송신한다.

2.6 요약
BPFDoor 의 악성 기능을 요약하면 아래와 같다.
- 정상 프로세스로 위장하여 시스템에 상주한다.
- 네트워크 인터페이스를 도청하며, 특수하게 조작된 명령 패킷이 오기를 기다린다.
- 명령 패킷을 전달받으면 이를 처리할 자식 프로세스를 생성한다.
- 자식 프로세스는 명령 패킷을 해석하여 아래 악성 행위 중 하나를 실행하고 종료한다.
- 리버스 셸 생성.
- 방화벽 규칙을 변경하여 특정 포트를 열고 공격자의 접속을 기다림.
- 지정된 대상에게 UDP Flooding 공격 실행.
3. 후기
BPFDoor 의 주된 기능은 감염시킨 시스템에 상주하며 공격자의 명령을 수행하는 것으로, 아주 전형적인 백도어 멀웨어다. 다른 백도어들과 다른 특이점은 Linux 시스템을 대상으로 작성된 멀웨어라는 점과, BPF 를 이용하여 특수하게 제작된 패킷만을 필터링한다는 점이다. 이렇게 특수 제작된 패킷들은 방화벽이나 WAF 의 차단 룰을 우회하거나, 탐지를 어렵게 만들 수 있다.
하지만 이와 같은 특이한 행위는 오히려 BPFDoor 를 탐지하기 쉽게 만드는 요인이 될 수 있다. 앞서 보았던 ss -0pb
명령어는 설정된 BPF를 확인할 수 있는데, 시스템 상황에 따라 다르겠지만 저토록 많은 필터를 거는 경우는 흔치 않기 때문에 오히려 명령어 한줄로 감염 여부를 확인할 수도 있을 것이다.