BPFDoor 분석

BPFDoor 분석

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

SKT 해킹 관련 뉴스

멀웨어 분석가로서 이런 이슈를 놓칠 수 없기에 분석해보기로 결정했다.

1. 샘플 수집

MalwareBazaar 에서 Signature 값이 BPFBoor 인 파일을 검색한 결과 중 하나를 선택하여 분석했다. 참고로 이 파일은 실제 SKT 해킹 사건과는 아무런 상관이 없다.

malwarebazaar 검색 결과

수집한 파일의 정보는 다음과 같다.

  • MD5: 156226c90974180cc4b5f9738e80f1f8
  • SHA-1: ed0cd45c3bb95ef8da214048799395e247040d17
  • SHA-256: 4c5cf8f977fc7c368a8e095700a44be36c8332462c0b1e41bff03238b2bf2a2d
  • 파일 크기: 27712 바이트
  • 파일 타입: ELF

2. 악성 행위 분석

2.1 중복 실행 방지

이 멀웨어는 초기에 /var/run/auditd.lock 의 존재 여부를 확인한 후, 해당 파일이 존재하면 악성 행위 없이 바로 종료된다.

lock 파일이 없을 시 프로세스 종료 코드

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

정상 프로세스 종료 시 lock 파일 삭제 코드

또한 해당 파일은 프로세스 정상 종료 시 발생하는 SIGTERN 시그널를 처리하는 핸들러 코드를 등록하는데, 이 코드는 /var/run/auditd.lock 를 삭제하는 기능을 가진다. 따라서 BPFDoor 가 종료될 시, /var/run/auditd.lock 파일은 삭제된다.

2.2 --init 인자 재실행

BPFDoor 는 관리자 권한으로 실행 시 /var/lock/ 경로에 kdumpflush 라는 이름으로 자기 자신을 복제한다. kdumpflush 라는 파일은 일반적으로 linux 에서 사용되는 이름은 아니지만, 리눅스 커널에서는 kdump 라는 이름의 정상적인 파일을 사용한다. BPFDoor 는 해당 파일과 유사한 이름으로 자기 자신을 복제한 후, --init 인자와 함께 실행된다.

kdumpflush 로 자기 자신 복제 후 실행 코드

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 설정 코드

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 명령어를 실행하면 네트워크 인터페이스에서 필터링하는 조건을 확인할 수 있다.

BPFDoor 실행 전 패킷 필터링 상태


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

BPFDoor 실행 후 패킷 필터링 상태

위와 같이 특정한 패킷만을 받도록 세팅한 후, 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 룰을 생성한다.

Port Knocker 코드

2.5.2 UPD 패킷 송신

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

UDP Flooding 으로 추정되는 코드

2.5.3 Reverse Shell 수립

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

리버스셸 관련 코드

2.6 요약

BPFDoor 의 악성 기능을 요약하면 아래와 같다.

  • 정상 프로세스로 위장하여 시스템에 상주한다.
  • 네트워크 인터페이스를 도청하며, 특수하게 조작된 명령 패킷이 오기를 기다린다.
  • 명령 패킷을 전달받으면 이를 처리할 자식 프로세스를 생성한다.
  • 자식 프로세스는 명령 패킷을 해석하여 아래 악성 행위 중 하나를 실행하고 종료한다.
    • 리버스 셸 생성.
    • 방화벽 규칙을 변경하여 특정 포트를 열고 공격자의 접속을 기다림.
    • 지정된 대상에게 UDP Flooding 공격 실행.

3. 후기

BPFDoor 의 주된 기능은 감염시킨 시스템에 상주하며 공격자의 명령을 수행하는 것으로, 아주 전형적인 백도어 멀웨어다. 다른 백도어들과 다른 특이점은 Linux 시스템을 대상으로 작성된 멀웨어라는 점과, BPF 를 이용하여 특수하게 제작된 패킷만을 필터링한다는 점이다. 이렇게 특수 제작된 패킷들은 방화벽이나 WAF 의 차단 룰을 우회하거나, 탐지를 어렵게 만들 수 있다.

하지만 이와 같은 특이한 행위는 오히려 BPFDoor 를 탐지하기 쉽게 만드는 요인이 될 수 있다. 앞서 보았던 ss -0pb 명령어는 설정된 BPF를 확인할 수 있는데, 시스템 상황에 따라 다르겠지만 저토록 많은 필터를 거는 경우는 흔치 않기 때문에 오히려 명령어 한줄로 감염 여부를 확인할 수도 있을 것이다.