eBPF로 BPFDoor 탐지하고 차단하기
들어가며
BPFDoor는 BPF(Berkeley Packet Filter)를 악용하는 리눅스 백도어입니다.
우리나라에서는 2025년 4월 발생한 대형 통신사 유심 정보 유출 사고에 사용된 해킹 도구로 알려져 있습니다.
| 항목 | 내용 |
|---|---|
| 피해 규모 | 약 2,696만 건 유심 정보 유출 |
| 감염 서버 | 28대 (악성코드 33종 발견) |
| 잠복 기간 | 2022년 6월 ~ 2025년 4월 (약 3년) |
이 글에서는 네트워크 레벨에 집중하여 BPFDoor의 매직 패킷 동작을 분석하고, eBPF/XDP로 탐지 및 차단하는 방법을 다룹니다.
(BPFDoor는 BPF 필터 외에도 위장 프로세스, 자가 복제 후 삭제 등 다양한 탐지 우회 기법을 사용합니다.)
💡 Note
이 글에서 다루는 BPFDoor-Defender 전체 소스 코드는 GitHub - kimmap/ebpf-bpfdoor-defender에서 확인할 수 있습니다.
1. BPFDoor 동작 원리
1.1 전체 아키텍처
BPFDoor의 동작은 크게 두 단계로 나뉩니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1단계: 잠복
BPFDoor 프로세스
├── RAW 소켓 생성 (PF_PACKET, SOCK_RAW)
├── BPF 필터 부착 (SO_ATTACH_FILTER)
└── 매직 패킷 대기 (무한 루프)
│
│ 매직 패킷 수신
▼
2단계: 활성화
│
├── 공격자 <─── 리버스 쉘 연결 ─── Target
│
└── 또는: 공격자 ──> iptables 조작 ──> 바인드 쉘
리버스 쉘 원리 (dup2):
1
2
3
4
5
// 소켓 fd를 표준 입출력으로 복제
dup2(sockfd, STDIN_FILENO); // 소켓 → 표준 입력
dup2(sockfd, STDOUT_FILENO); // 소켓 → 표준 출력
dup2(sockfd, STDERR_FILENO); // 소켓 → 표준 에러
execl("/bin/sh", "sh", NULL); // 쉘 실행 → 모든 입출력이 공격자에게 전달
1.2 왜 탐지가 어려운가?
일반적인 백도어와 BPFDoor의 차이점:
| 항목 | 일반 백도어 | BPFDoor |
|---|---|---|
| 리스닝 포트 | 열림 (예: 4444) | 없음 |
| netstat 탐지 | 가능 | 불가능 |
| 방화벽 우회 | 어려움 | 쉬움 |
| 트래픽 특성 | 지속적 연결 | 단발성 트리거 |
BPFDoor는 RAW 소켓을 사용하여 모든 IP 패킷의 복사본을 수신합니다. 커널 수준에서 BPF 필터가 매직 패킷만 골라내므로, 유저스페이스에서는 관련 포트가 전혀 보이지 않습니다.
2. 매직 패킷 구조 분석
2.1 패킷 캡처
tcpdump로 실제 BPFDoor 매직 패킷을 캡처해 보았습니다:
1
2
3
4
5
07:24:10.913005 IP 192.168.57.101.47857 > 192.168.57.100.29269: UDP, length 24
0x0000: 4500 0034 719a 4000 4011 d504 c0a8 3965 E..4q.@.@.....9e
0x0010: c0a8 3964 baf1 7255 0020 1ca0 7255 0000 ..9d..rU....rU..
0x0020: ffff ffff 2329 6a75 7374 666f 7266 756e ....#)justforfun
0x0030: 0000 0000 ....
2.2 패킷 구조 분석
IP 헤더 (20 bytes)
1
2
3
4
5
6
4500 0034 719a 4000 4011 d504 c0a8 3965 c0a8 3964
││││ ││││ │││││││││ │││││││││
││││ ││││ │││││││││ └───────┴─ Dst: 192.168.57.100
││││ ││││ └───────┴─────────── Src: 192.168.57.101
││││ └┴┴┴──────────────────────────────────────── Total Length: 52
└┴┴┴───────────────────────────────────────────── IPv4, IHL=5
UDP 헤더 (8 bytes)
1
2
3
4
5
baf1 7255 0020 1ca0
││││ ││││ ││││
││││ ││││ └┴┴┴─── Length: 32 bytes
││││ └┴┴┴──────── Dst Port: 29269 (0x7255) ★ 트리거 포트
└┴┴┴───────────── Src Port: 47857 (임의)
매직 패킷 페이로드 (24 bytes)
BPFDoor가 정의한 magic_packet 구조체:
1
2
3
4
5
6
struct magic_packet {
unsigned int flag; // 4 bytes - 매직 플래그
in_addr_t ip; // 4 bytes - 콜백 IP
unsigned short port; // 2 bytes - 콜백 포트
char pass[14]; // 14 bytes - 패스워드
} __attribute__ ((packed));
실제 데이터 매핑:
1
2
3
4
5
6
7255 0000 ffff ffff 2329 6a75 7374 666f 7266 756e 0000 0000
│││││││││ │││││││││ ││││ │││││││││││││││││││││││││││││││││
│││││││││ │││││││││ ││││ └───────────────────────────────┴─ pass: "justforfun"
│││││││││ │││││││││ └┴┴┴─────────────────────────────────── port: 9001
│││││││││ └───────┴──────────────────────────────────────── ip: 0xFFFFFFFF (src 사용)
└───────┴────────────────────────────────────────────────── flag: 0x72550000 ★
2.3 핵심 탐지 시그니처
| 필드 | 값 | 설명 |
|---|---|---|
| UDP Dst Port | 29269 (0x7255) | 트리거 포트 |
| Payload[0:4] | 0x72550000 | 매직 플래그 |
| Payload[4:8] | 0xFFFFFFFF | 콜백 IP (패킷 src 사용) |
| Payload[8:10] | 가변 | 콜백 포트 |
| Payload[10:24] | 가변 | 인증 패스워드 |
3. BPF 필터 코드 분석
BPFDoor는 classic BPF(cBPF) 필터를 사용하여 매직 패킷만 선별합니다.
3.1 필터 코드 원본
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sock_filter bpf_code[] = {
{ 0x28, 0, 0, 0x0000000c }, // [0] A = EtherType
{ 0x15, 0, 27, 0x00000800 }, // [1] if (A != IP) goto REJECT
{ 0x30, 0, 0, 0x00000017 }, // [2] A = IP.Protocol
{ 0x15, 0, 5, 0x00000011 }, // [3] if (A != UDP) goto ICMP_CHECK
{ 0x28, 0, 0, 0x00000014 }, // [4] A = IP.FragOffset
{ 0x45, 23, 0, 0x00001fff }, // [5] if (fragmented) goto REJECT
{ 0xb1, 0, 0, 0x0000000e }, // [6] X = IP.HeaderLen
{ 0x48, 0, 0, 0x00000016 }, // [7] A = Payload[0:2]
{ 0x15, 19, 20, 0x00007255 }, // [8] if (A == 0x7255) ACCEPT else REJECT
// ... ICMP, TCP 체크 생략 ...
{ 0x6, 0, 0, 0x0000ffff }, // [28] return ACCEPT
{ 0x6, 0, 0, 0x00000000 }, // [29] return REJECT
};
3.2 핵심 로직 해석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
EtherType 체크
│
├── != 0x0800 (IP) ──> REJECT
│
└── == 0x0800 (IP)
│
▼
Protocol 체크
│
├── UDP(17) ──> Fragment? ──> Payload[0:2] == 0x7255? ──> ACCEPT
│
├── ICMP(1) ──> Fragment? ──> Payload[0:2] == 0x7255? ──> ACCEPT
│
└── TCP(6) ──> Fragment? ──> Payload[0:2] == 0x5293? ──> ACCEPT
3.3 프로토콜별 매직 값
| 프로토콜 | 매직 값 | 위치 |
|---|---|---|
| UDP | 0x7255 | 페이로드 첫 2바이트 |
| ICMP | 0x7255 | 데이터 첫 2바이트 |
| TCP | 0x5293 | 페이로드 첫 2바이트 |
4. 실제 공격/방어 테스트
4.1 테스트 환경
1
2
3
4
5
┌─────────────────┐ ┌───────────────────┐
│ Attacker │ ────── UDP Magic Packet ──────> │ Target │
│ 192.168.57.101 │ <───── Reverse Shell ───────── │ 192.168.57.100 │
│ │ │ (bpfdoor_defender)│
└─────────────────┘ └───────────────────┘
- VM 2대: VirtualBox + Vagrant
- OS: Rocky Linux 9.4
- Kernel: 5.14.0-427.13.1.el9_4.x86_64
4.2 Case 1: 방어 시스템 없음 (공격 성공)
Defender가 없는 상태에서 공격을 수행하면 리버스 쉘이 성공적으로 연결됩니다.
[Target] BPFDoor 실행:
1
2
3
$ git clone https://github.com/gwillgues/BPFDoor.git
$ gcc -o bpfdoor bpfdoor.c
$ ./bpfdoor --init
[Attacker] 공격 실행:
💡 Note
공격 프로그램(bpfdoor_attacker)은 테스트 목적으로 별도 작성하였으며, 악용 위험이 있어 공개하지 않습니다.
1
2
3
4
5
6
7
8
9
10
$ ./bpfdoor_attacker --target-ip 192.168.57.100
[INFO] Executing Reverse Shell Attack against 192.168.57.100
[INFO] Listening for reverse shell on local port: 9001
[INFO] UDP Magic packet sent (src_port:0 -> 192.168.57.100:29269) with password 'justforfun'
[INFO] Reverse shell connection accepted from 192.168.57.100:50418
[INFO] Read and discarded initial handshake: "3458"
[INFO] Starting interactive shell I/O (RC4 encrypted)...
[root@target /]$ whoami
root
[Target] tcpdump로 매직 패킷 확인:
1
2
3
4
5
6
$ tcpdump -i eth1 -nn 'udp and port 29269' -X
08:02:46.822257 IP 192.168.57.101.41864 > 192.168.57.100.29269: UDP, length 24
0x0000: 4500 0034 5b34 4000 4011 eb6a c0a8 3965 E..4[4@.@..j..9e
0x0010: c0a8 3964 a388 7255 0020 3409 7255 0000 ..9d..rU..4.rU..
0x0020: ffff ffff 2329 6a75 7374 666f 7266 756e ....#)justforfun
0x0030: 0000 0000 ....
결과: 매직 패킷 하나로 root 권한의 암호화된 리버스 쉘 획득
4.3 Case 2: Defender 활성화 (공격 차단)
💡 Note
BPFDoor-Defender 전체 소스 코드는 GitHub - kimmap/ebpf-bpfdoor-defender에서 확인할 수 있습니다.
Step 1: eBPF 프로그램 컴파일
1
2
3
4
$ clang -O2 -g -target bpf -c bpfdoor_defender.bpf.c -o bpfdoor_defender.bpf.o
$ file bpfdoor_defender.bpf.o
bpfdoor_defender.bpf.o: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), with debug_info, not stripped
Step 2: eBPF 바이트코드 확인 (선택)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ llvm-objdump -S bpfdoor_defender.bpf.o
Disassembly of section xdp:
0000000000000000 <bpfdoor_defender>:
; if (bpf_ntohs(eth->h_proto) == ETH_P_IP);
7: 56 02 29 00 08 00 00 00 if w2 != 0x8 goto +0x29
; return (iph->protocol == IPPROTO_UDP);
18: 56 02 1e 00 11 00 00 00 if w2 != 0x11 goto +0x1e
; return (bpf_ntohs(*magic) == BPFDOOR_MAGIC);
43: 56 01 05 00 72 55 00 00 if w1 != 0x5572 goto +0x5 # 0x7255 매직!
; bpf_printk("BPFDOOR: Magic packet detected! DROP");
47: 85 00 00 00 06 00 00 00 call 0x6
48: b4 00 00 00 01 00 00 00 w0 = 0x1 # XDP_DROP
Step 3: XDP 프로그램 Attach
1
2
3
4
5
6
7
8
9
10
11
12
13
# attach 전 - bpfdoor_defender 없음
$ bpftool prog list | grep xdp
(없음)
# XDP attach
$ ip link set dev eth1 xdp obj bpfdoor_defender.bpf.o sec xdp
# attach 후 - id 49로 로드됨
$ bpftool prog list | grep -A3 "xdp.*bpfdoor"
49: xdp name bpfdoor_defender tag 6c59633e1a31ba4a gpl
loaded_at 2026-02-03T08:10:02+0900 uid 0
xlated 400B jited 263B memlock 4096B map_ids 13
btf_id 77
Step 4: 프로그램 상세 정보
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ bpftool prog show id 49 --pretty
{
"id": 49,
"type": "xdp",
"name": "bpfdoor_defender",
"tag": "6c59633e1a31ba4a",
"gpl_compatible": true,
"jited": true,
"bytes_xlated": 400,
"bytes_jited": 263
}
$ bpftool net list
xdp:
eth1(3) generic id 49
Step 5: 공격 시도 및 탐지
[Attacker]
1
2
3
4
$ ./bpfdoor_attacker --target-ip 192.168.57.100
[INFO] UDP Magic packet sent (src_port:0 -> 192.168.57.100:29269)
[INFO] Waiting for reverse shell connection or timeout...
# 연결 타임아웃 - 패킷이 DROP됨
[Target] 탐지 로그:
1
2
$ cat /sys/kernel/debug/tracing/trace_pipe
<idle>-0 [000] ..s2. 1885.217886: bpf_trace_printk: BPFDOOR: Magic packet detected! DROP
[Target] tcpdump - XDP에서 DROP되어 패킷 안 보임:
1
2
3
$ tcpdump -i eth1 -nn 'udp and port 29269'
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
# (패킷 없음 - XDP에서 커널 스택 도달 전 DROP)
4.4 결과 비교
| 항목 | Defender OFF | Defender ON |
|---|---|---|
| Magic Packet 도달 | ✅ 커널 통과 | ❌ XDP에서 DROP |
| 리버스 쉘 연결 | ✅ 성공 | ❌ 타임아웃 |
| tcpdump 탐지 | ✅ 패킷 보임 | ❌ 패킷 안 보임 |
| 공격 소요 시간 | ~5ms | N/A (차단) |
XDP의 강점: 커널 네트워크 스택에 도달하기 전에 DROP하므로, tcpdump로도 패킷을 볼 수 없습니다.
5. 결론 및 추가 탐지 방안
5.1 구현 결과
eBPF를 활용하여 BPFDoor의 매직 패킷을 커널 수준에서 탐지하고 차단하는 시스템을 구현했습니다.
| 항목 | 결과 |
|---|---|
| UDP 매직 패킷 탐지 | ✅ |
| 실시간 패킷 DROP | ✅ |
| 이벤트 로깅 | ✅ |
| 통계 수집 | ✅ |
5.2 마무리
BPFDoor는 BPF 기술을 교묘하게 악용한 백도어입니다.
하지만 같은 기술의 발전형인 eBPF를 사용하면 커널 수준에서 효과적으로 탐지하고 차단할 수 있습니다.