기존
/remote-support/WebRTC 화면공유 인프라를 확장하여 원격 키보드/마우스 제어 기능을 추가.
고객은 전용 Windows .exe 다운로드, 관리자는 기존 브라우저 뷰어에서 그대로 제어.
wp-content/plugins/wellcoms-remote/ (v2.0.0 → v3.0.0)wellcoms-remote-client/ (Windows .exe, 별도 GitLab 리포)/remote-support/ 페이지는 Phase 1(화면 공유/시청)이 완성된 상태로 운영 중이며, 개발문서 12장에 명시된 Phase 3 "원격 조작 (포터블 .exe 필요)" 단계에 해당한다.
고객이 다운로드한 Windows .exe를 실행하면 기존과 동일하게 4자리 PIN이 발급되고, 관리자가 PIN으로 연결하여 화면 시청 + 키보드/마우스 제어를 수행할 수 있도록 한다. 단, 기존 브라우저 기반 "화면만 보기" 경로는 100% 보존한다(fallback 용도).
| 원칙 | 내용 |
|---|---|
| 기존 인프라 최대 재사용 | 시그널링 서버/TURN/플러그인 구조는 그대로, 단 시그널링 서버는 관리자 토큰 검증 로직 추가 필요 (기존 Phase 1의 보안 구멍을 Phase 3에서 수습) |
| 관리자 UX 동일 | 기존 브라우저 PIN 입력 → 화면 시청 흐름 유지, "제어"는 토글 |
| 고객 경로 2종 유지 | (A) 브라우저만 — 화면 보기 / (B) .exe 다운로드 — 화면 + 제어 |
| 보안 최우선 | 관리자 토큰 인증, 사용자 승인, 세션 표시, 만료, 감사 로그 |
| 점진적 확장 | v1 = 최소 제어 / v2 = 파일전송·클립보드 / v3 = 음성·UAC |
⚠️ 기존 "서버 코드 0줄 변경" 주장은 철회됨. Momus 검토 결과 시그널링 서버가 관리자를 인증하지 않는 보안 구멍 발견. 자세한 내용은 §4.4 참조.
다음 항목 모두 충족 시 v1.0 출시:
| # | 기준 | 검증 방법 |
|---|---|---|
| 1 | Windows 11 + Windows 10 22H2에서 .exe 정상 실행 | 테스트 매트릭스 통과 |
| 2 | PIN 발급 후 관리자 브라우저에서 30초 내 화면 표시 | E2E 테스트 |
| 3 | 관리자 "원격 제어" 토글 → 고객 승인 → 마우스 클릭/키보드 입력 E2E 동작 | 수동 시나리오 |
| 4 | 입력 지연 < 150ms (P2P) / < 250ms (TURN 릴레이) | chrome://webrtc-internals 측정 |
| 5 | 관리자 토큰 없는 join_room 시도 → 거부 (PIN 유출 시에도 안전) |
보안 테스트 |
| 6 | 고객이 "제어 중지" 클릭 → 1초 내 입력 채널 차단 | 수동 시나리오 |
| 7 | PIN 5회 오입력 → 5분 임금 (기존 동작 유지) | 자동화 테스트 |
| 8 | 30분 세션 자동 종료 | 자동화 테스트 |
| 9 | 기존 브라우저 "화면만 보기" 경로 100% 동작 (회귀 없음) | 회귀 테스트 |
| 10 | VirusTotal 70개 이상 AV 통과 (false positive 없음) | 제출 전 확인 |
| 11 | 주요 브라우저(Chrome/Edge/Firefox 최신) 입력 캡처 정상 | 브라우저 매트릭스 |
| 12 | 세션 로그 DB 기록 (pin, ip, 시간, 제어 여부) | DB 확인 |
| 역할 | 행동 | 필요 조건 |
|---|---|---|
| 고객 (제어당함) | /remote-support/ → .exe 다운로드 → 실행 → PIN 안내 → 승인 |
Windows 10 1903+ |
| 관리자 (제어함) | /remote-support/ (로그인) → PIN 입력 → "원격 제어" 토글 ON → 마우스/키보드 조작 |
WP 관리자 권한, 데스크탑 브라우저 |
| 범위 | 항목 |
|---|---|
| ✅ In | Windows .exe 클라이언트 / WebRTC DataChannel 입력 채널 / 관리자 뷰어 입력 오버레이 / 사용자 승인 프롬프트 / 세션 빨간 테두리 표시 / 기본 감사 로그 |
| ❌ Out (v1) | macOS/Linux 클라이언트 / 음성 통화 / 파일 전송 / 클립보드 동기화 / UAC 권한 앱 제어 / 세션 녹화 / 다중 모니터 전환 UI / 자동 업데이트 (수동 배포만) |
graph TB
subgraph "고객 Windows"
EXE["wellcoms-remote-client.exe
(C# .NET 8 + WPF)"]
CAP["Windows.Graphics.Capture
화면 캡처 (하드웨어 가속)"]
SI["user32!SendInput
키보드/마우스 주입"]
EXE --> CAP
EXE --> SI
end
subgraph "wellcoms.co.kr 서버 (대부분 기존 그대로)"
WP["wellcoms_wp
WordPress + wellcoms-remote 플러그인 v3.0"]
SIG["wellcoms_signal
Node.js WebSocket :3000
(관리자 토큰 검증 추가 — §4.4)"]
TURN["wellcoms_turn
coturn :3478 (변경 없음)"]
end
subgraph "관리자 브라우저 (기존 뷰어 확장)"
ADM["/remote-support/
(관리자 로그인)"]
VIEW["📺 비디오 뷰어 (기존)"]
INPUT["🖱️⌨️ 입력 오버레이 (신규)"]
ADM --> VIEW
ADM --> INPUT
end
CAP -->|"WebRTC Video Track"| VIEW
INPUT -->|"WebRTC DataChannel 'input'"| SI
EXE -->|"WSS 시그널링"| SIG
ADM -->|"WSS 시그널링"| SIG
CAP -.->|"P2P/TURN 릴레이"| VIEW
INPUT -.->|"P2P/TURN 릴레이"| SI
graph LR
subgraph "PeerConnection (1개)"
VT["Video Track
(고객 → 관리자)"]
DC_VID["DataChannel 'ctrl'
(양방향 제어 메시지)"]
DC_IN["DataChannel 'input'
(관리자 → 고객 입력 이벤트)"]
end
EXE_C["고객 .exe"] --> VT
EXE_C --> DC_VID
ADM_C["관리자 브라우저"] --> DC_VID
ADM_C --> DC_IN
DC_IN --> EXE_C
핵심: 기존 비디오 트랙 1개에 DataChannel 2개를 추가한다. PeerConnection 자체는 1개이므로 NAT/TURN 협상 로직이 그대로 재사용된다.
기존 docker-compose.yml에 WR_SECRET 환경변수만 추가:
services:
wellcoms_db: ... # MariaDB (동일)
wordpress: ... # WordPress (플러그인만 v3.0으로 업그레이드)
wellcoms_signal:
image: wellcoms-signal:latest
environment:
SIGNAL_PORT: "3000"
WR_SECRET: ${WR_SECRET} # 신규 — WordPress와 공유 (HMAC 서명용, §4.4)
wellcoms_turn: ... # TURN (동일)
신규 컨테이너 없음, 신규 포트 없음. signal 서버 이미지만 코드 수정 후 rebuild.
| 타입 | 방향 | 설명 | v1 변경 |
|---|---|---|---|
create_room |
클라→서버 | 방 생성 | 동일 |
join_room {pin} |
클라→서버 | PIN 참여 | {pin, token} 으로 확장 — 토큰 없으면 view만 (§4.4) |
sdp_offer/answer |
릴레이 | WebRTC 협상 | 동일 |
ice_candidate |
릴레이 | ICE 후보 교환 | 동일 |
| 채널 | 타입 | 방향 | 페이로드 |
|---|---|---|---|
input |
mouse_move |
관리자→고객 | {x, y} (절대 좌표, 원격 해상도 기준) |
input |
mouse_down |
관리자→고객 | {button: 'left'\|'right'\|'middle'} |
input |
mouse_up |
관리자→고객 | {button} |
input |
mouse_wheel |
관리자→고객 | {deltaX, deltaY} |
input |
key_down |
관리자→고객 | {vk: 'VK_RETURN', code: 13} |
input |
key_up |
관리자→고객 | {vk, code} |
ctrl |
request_control |
관리자→고객 | {} 제어 권한 요청 |
ctrl |
control_granted |
고객→관리자 | {} 승인됨 |
ctrl |
control_denied |
고객→고객 | {} 거절됨 |
ctrl |
control_revoked |
고객→관리자 | {} 고객이 제어 중단 |
ctrl |
hello |
양방향 | {version, capabilities} 핸드셰이크 |
| 계층 | 기술 | 이유 |
|---|---|---|
| 언어/런타임 | C# .NET 8 | Windows API 접근 최고, 단일 exe 배포 용이 |
| UI 프레임워크 | WPF | PIN 표시 + 상태 표시에 충분, 간단한 UI |
| WebRTC | SIPSorcery (sipsorcery-media) |
C# WebRTC 라이브러리, Apache 2.0, 비디오 트랙 + DataChannel 지원 |
| 화면 캡처 | Windows.Graphics.Capture API | Win10 1903+ 공식, 하드웨어 가속, 커서 포함, 다중 모니터 지원 |
| 입력 주입 | user32!SendInput P/Invoke | 시스템 레벨 키/마우스, UAC 권한 앱 외 전부 |
| 인코딩 | H.264 (MediaFoundation) | 하드웨어 인코딩, 1080p 30fps 5Mbps 가능 |
| 배포 | Single-file publish + ZIP | .exe 단일 파일 (~30-50MB) + ZIP 묶음 |
graph LR
DWM["DWM Compositor"] --> GC["GraphicsCaptureItem"]
GC --> DF["Direct3D11CaptureFramePool"]
DF --> TXT["D3D11Texture2D
(GPU 메모리)"]
TXT --> ENC["MediaFoundation H.264 Encoder
(하드웨어 가속)"]
ENC --> RTS["RTP 패킷"]
RTS --> PC["RTCRtpSender"]
PC -.->|"WebRTC Video Track"| ADM_C["관리자 브라우저"]
포인트: Direct3D11 텍스처를 H.264로 직접 인코딩 → CPU/GPU 오버헤드 최소화.
// 마우스 무브 (절대 좌표 0..65535 정규화)
private static void MouseMove(int x, int y) {
var input = new INPUT {
type = INPUT_MOUSE,
u = new InputUnion {
mi = new MOUSEINPUT {
dx = x * 65535 / Screen.PrimaryScreen.Bounds.Width,
dy = y * 65535 / Screen.PrimaryScreen.Bounds.Height,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE
}
}
};
SendInput(1, ref input, Marshal.SizeOf(input));
}
// 키보드 (가상 키 코드)
private static void KeyPress(ushort vk, bool down) {
var input = new INPUT {
type = INPUT_KEYBOARD,
u = new InputUnion {
ki = new KEYBDINPUT {
wVk = vk,
dwFlags = down ? 0 : KEYEVENTF_KEYUP
}
}
};
SendInput(1, ref input, Marshal.SizeOf(input));
}
stateDiagram-v2
[*] --> Launching: exe 실행
Launching --> Connecting: WSS 연결 시도
Connecting --> Waiting: create_room → PIN 발급
Waiting --> Connected: 관리자 입장 (peer_joined)
Connected --> Signaling: SDP/ICE 교환
Signaling --> Streaming: P2P 연결 완료
Streaming --> AwaitingApproval: 관리자 request_control
AwaitingApproval --> Controlled: 사용자 승인
AwaitingApproval --> Streaming: 사용자 거절/타임아웃
Controlled --> Streaming: 관리자/사용자 제어 중지
Controlled --> Controlled: 입력 이벤트 수신
Streaming --> Ended: 연결 종료
Waiting --> Ended: 30분 타임아웃
Ended --> [*]
┌──────────────────────────────────────┐
│ 웰컴스 원격 지원 [_][□][×]│
├──────────────────────────────────────┤
│ │
│ 4 8 2 7 │
│ (PIN, 48pt 굵게) │
│ │
│ 📺 화면 공유 중 (관리자 대기) │
│ │
│ [복사] [소리로 듣기] │
│ │
│ ─────────────────────────────────────│
│ ℹ️ 이 번호를 전화로 안내해주세요 │
│ ⚠️ 원격 제어 요청 시 이 창에서 │
│ 반드시 [허용]을 눌러주세요 │
│ │
│ [공유 중지] │
└──────────────────────────────────────┘
기존 shortcode [wellcoms_screen_share] 내부에 다음을 추가:
<video> 위 입력 오버레이 (투명 div로 마우스 캡처)// 비디오 위에 투명 div를 올려서 모든 입력 이벤트 가로채기
const overlay = document.createElement('div');
overlay.className = 'wr-input-overlay';
overlay.style.display = 'none'; // 기본 OFF
// 토글 ON 시
function enableControl() {
if (!inputDC || inputDC.readyState !== 'open') {
toast('제어 채널이 없습니다', 'error'); return;
}
// 고객에게 제어 권한 요청
inputDC.send(JSON.stringify({type: 'request_control'}));
// 고객 승인 대기 모달 표시
showControlRequestModal();
}
overlay.addEventListener('mousemove', e => {
const rect = video.getBoundingClientRect();
const scaleX = video.videoWidth / rect.width;
const scaleY = video.videoHeight / rect.height;
const x = Math.floor((e.clientX - rect.left) * scaleX);
const y = Math.floor((e.clientY - rect.top) * scaleY);
inputDC.send(JSON.stringify({type: 'mouse_move', x, y}));
});
overlay.addEventListener('mousedown', e => {
const btn = e.button === 0 ? 'left' : e.button === 2 ? 'right' : 'middle';
inputDC.send(JSON.stringify({type: 'mouse_down', button: btn}));
});
// wheel, keydown, keyup, mouseup, contextmenu(차단) 유사 패턴
graph LR
MM["관리자 마우스
clientX, clientY"] --> RECT["video.getBoundingClientRect()"]
RECT --> SCALE["videoWidth / rect.width"]
SCALE --> NORM["원격 해상도 기준 x, y"]
NORM --> DC["DataChannel mouse_move"]
DC --> EXE["고객 .exe 수신"]
EXE --> SI["SendInput 절대 좌표 (0..65535)"]
주의점:
object-fit: contain이면 레터박스 영역은 무시해야 함 (rect와 실제 비디오 영역이 다름)⚠️ 기존 "변경 없음" 주장은 철회. §4.4에서 설명한 보안 구멍(누구나 PIN으로 join 가능)을 막기 위해 관리자 토큰 검증 로직이 추가되어야 한다.
signal/server.js의 join_room 케이스에 토큰 검증을 추가:
// AS-IS (취약)
case 'join_room': {
const pin = String(msg.pin || '');
// ... PIN만 체크
}
// TO-BE (안전)
case 'join_room': {
const pin = String(msg.pin || '');
const adminPayload = verifyAdminToken(msg.token);
if (!adminPayload) {
send(ws, { type: 'error', message: '관리자 인증 필요' });
ws.close(1008, 'unauthorized');
return;
}
ws._admin_id = adminPayload.admin_id;
ws._admin_name = adminPayload.admin_name;
// ... 이후 기존 로직
send(room.host, { type: 'peer_joined', admin_name: adminPayload.admin_name });
}
wellcoms_signal 컨테이너에 환경변수 추가:
wellcoms_signal:
image: wellcoms-signal:latest
environment:
SIGNAL_PORT: "3000"
WR_SECRET: ${WR_SECRET} # WordPress와 공유 (HMAC 서명용)
.env 파일에 WR_SECRET=<랜덤 64바이트 hex> 추가.
토큰에서 얻은 admin_id를 DB 세션 로그에 기록:
// peer_joined 시
db.query('INSERT INTO wp_wr_sessions SET ?', {
pin, customer_ip: host._ip, admin_user_id: adminPayload.admin_id,
started_at: new Date()
});
| 위치 | 기능 |
|---|---|
/remote-support/ 고객 페이지 |
"원격 제어용 다운로드" 버튼 추가 (기존 "즉시 화면 공유"는 유지) |
/remote-support/ 관리자 페이지 |
"원격 제어" 토글 버튼, 입력 캡처 활성화 |
| WP Admin → 원격지원 | 다운로드 URL 설정, 버전 표시, 세션 로그 뷰어 |
REST API /wp-json/wr/v1/client/version |
최신 버전 + 다운로드 URL 반환 (자동 업데이트용, v1에서는 하드코딩 가능) |
CREATE TABLE wp_wr_sessions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
pin VARCHAR(4) NOT NULL,
customer_ip VARCHAR(45),
admin_user_id BIGINT,
started_at DATETIME,
ended_at DATETIME,
control_enabled TINYINT DEFAULT 0,
control_duration_sec INT DEFAULT 0,
exit_reason VARCHAR(32), -- 'admin_disconnect' | 'customer_disconnect' | 'timeout' | 'error'
INDEX idx_pin (pin),
INDEX idx_started (started_at)
);
로깅 시점: create_room 시 INSERT, peer_left/cleanup 시 UPDATE.
유료 코드 서명 인증서($200-400/년) 없이 Windows .exe를 배포할 때:
| 옵션 | 비용 | UX 평점 | SmartScreen 우회 | AV false positive | 비고 |
|---|---|---|---|---|---|
| Microsoft Trusted Signing | $9.99/월 | ⭐⭐⭐⭐⭐ | ✅ (MS 자체 인증서) | 낮음 | Azure 클라우드 서명, 파트너 센터 계정 필요 (1-2주 셋업) |
| Certum Open Source | ~$30/년 | ⭐⭐⭐ | ⚠️ 평판 축적 1-3개월 | 보통 | OV 인증서, 사업자 등록 불필요 (개인 OK) |
| Sectigo/SSL.com OV | $80-200/년 | ⭐⭐⭐⭐ | ⚠️ 평판 축적 1-3개월 | 낮음 | 사업자 확인 필요 |
| .zip 묶음 + 안내 | 무료 | ⭐⭐ | ❌ (사용자 클릭 유도) | 보통 | 현재 가장 현실적 무료 옵션 |
| MSIX + Microsoft Store | 무료 + $19 일회 | ⭐⭐⭐⭐ | ✅ (MS 서명) | 낮음 | 샌드박스 제한으로 SendInput 불가 → 부적합 |
| PowerShell 부트스트랩 | 무료 | ⭐⭐ | ❌ (MOTW 여전함) | 보통 | .cmd 파일이 PS 다운로드/실행 |
| 자체 서명 + 인증서 설치 | 무료 | ⭐ | ❌ | 높음 | 사용자에게 인증서 설치 요구 → 부적합 |
1단계 (v1 출시, 무료): .zip 묶음 + 다운로드 페이지 상세 안내
wellcoms-remote-v1.zip 다운로드 (내부에 .exe 1개 + 사용법.txt)/remote-support/ 페이지에 다운로드 방법 스크린샷 + 음성 안내2단계 (v1.1+, $10/월): Microsoft Trusted Signing 도입
requestedExecutionLevel="asInvoker" (UAC 프롬프트 회피).NET trim + PublishSingleFile=true (의존성 누락 방지)libdatachannel로 전환 검토┌──────────────────────────────────────────────────┐
│ 🖥️ 원격 지원 — Windows 프로그램 다운로드 │
├──────────────────────────────────────────────────┤
│ │
│ [💼 wellcoms-remote-v1.zip 다운로드] (28MB) │
│ │
│ 📋 사용 순서: │
│ 1. 위 버튼 클릭 → 다운로드 │
│ 2. 다운로드된 ZIP 파일 압축 해제 │
│ 3. wellcoms-remote.exe 실행 │
│ 4. 화면에 표시된 4자리 번호를 전화로 안내 │
│ │
│ ⚠️ "Windows가 PC를 보호했습니다" 파란 창이 뜨면: │
│ → [추가 정보] 클릭 │
│ → [실행] 버튼 클릭 │
│ │
│ 📞 문의: 02-XXXX-XXXX │
│ │
└──────────────────────────────────────────────────┘
sequenceDiagram
actor C as 고객
participant EXE as .exe 클라이언트
participant SIG as 시그널링 서버
participant ADM as 관리자 브라우저
actor A as 관리자
Note over C,A: Phase 1 — PIN 발급
C->>EXE: wellcoms-remote.exe 실행
EXE->>SIG: WSS 연결
EXE->>SIG: create_room
SIG-->>EXE: room_created {pin: "4827"}
EXE-->>C: PIN 4827 표시
C->>A: (전화) "4827번입니다"
Note over C,A: Phase 2 — 관리자 연결
A->>ADM: /remote-support/ 접속 (로그인)
A->>ADM: PIN 4827 입력 + 연결
ADM->>SIG: join_room {pin}
SIG-->>EXE: peer_joined
SIG-->>ADM: peer_joined
Note over C,A: Phase 3 — WebRTC 협상
EXE->>SIG: sdp_offer (video track + DataChannels)
SIG->>ADM: sdp_offer
ADM->>SIG: sdp_answer
SIG->>EXE: sdp_answer
Note over EXE,ADM: ICE candidates 교환
Note over C,A: Phase 4 — 스트리밍 + 제어
EXE-->>ADM: Video Track (고객 화면)
ADM-->>A: graph LR
subgraph "위협"
T1["무단 제어"]
T2["PIN 브루트포스"]
T3["세션 하이재킹"]
T4["입력 인젝션 악용"]
T5["변조된 exe 배포"]
end
subgraph "대응"
D1["WP 관리자 인증 + 승인 프롬프트"]
D2["PIN 5회 실패 시 5분 차단 (기존)"]
D3["WSS TLS + SRTP (기존)"]
D4["제어는 명시적 허용 후에만, 언제든 철회 가능"]
D5["HTTPS 다운로드 + SHA256 체크섬 게시"]
end
T1 --> D1
T2 --> D2
T3 --> D3
T4 --> D4
T5 --> D5
| 항목 | 구현 | v1 |
|---|---|---|
| Signaling 서버 WSS | 기존 (Caddy TLS) | ✅ |
| 관리자 인증 | MangBoard 레벨 10+ 또는 manage_options (기존) | ✅ |
| PIN 만료 | 30분 (기존) | ✅ |
| PIN 시도 제한 | IP당 5회/5분 (기존) | ✅ |
| WebRTC 암호화 | SRTP (기존) | ✅ |
| 사용자 승인 모달 | 관리자 request_control 시 고객 화면에 모달 | ✅ 신규 |
| 제어 철회 버튼 | 고객이 언제든 "제어 중지" 클릭 | ✅ 신규 |
| 세션 빨간 테두리 | 제어 중일 때 고객 화면 4면 빨간 테두리 | ✅ 신규 |
| 감사 로그 | DB에 세션 기록 (pin, ip, 시간, 제어 여부) | ✅ 신규 |
| exe 체크섬 공개 | 다운로드 페이지에 SHA256 표시 | ✅ 신규 |
| DataChannel 인증 | hello 메시지로 버전/역할 검증 | ⚠️ v1.1 |
| 세션 자동 종료 | 60분 강제 종료 (옵션) | ⚠️ v1.1 |
┌──────────────────────────────────────┐
│ ⚠️ 원격 제어 요청 │
├──────────────────────────────────────┤
│ │
│ 상담원이 회원님의 컴퓨터를 │
│ 직접 조작하려고 합니다. │
│ │
│ • 마우스와 키보드를 제어할 수 있습니다 │
│ • 언제든 [제어 중지]로 중단 가능합니다 │
│ • 세션 종료 시 모든 권한이 사라집니다 │
│ │
│ [거절] [10초 후 자동 거절] │
│ [✓ 허용] │
│ │
└──────────────────────────────────────┘
기본 동작: 15초 내 응답 없으면 자동 거절 (보수적 설계).
승인 모달에 표시되는 관리자 정보 (토큰 인증 도입 시 — §4.4):
✓ wellcoms.co.kr 인증된 관리자
이름: 김철수 (jskim)
접속 시각: 2026-06-18 14:32
⚠️ 이 섹션은 Momus 검토에서 발견된 critical 보안 구멍에 대한 대응이다.
기존signal/server.js는join_room {pin}메시지를 받으면 PIN만 맞으면 무조건 viewer로 등록한다.
WordPress의wr_is_admin()체크는 페이지 렌더링 시점에만 수행되며, WS 연결 자체를 인증하지 않는다.
Phase 1(보기 전용)에서는 감수 가능했으나, Phase 3(제어 권한 부여)에서는 심각한 취약점이 된다.
1. 공격자가 정당한 고객의 PIN을 우연히/사회공학적으로 입수
(예: 전화 도청, 화면 캡처, PIN 재사용 등)
2. 공격자가 브라우저 개발자 도구 또는 커스텀 WS 클라이언트로
wss://wellcoms.co.kr/signal/ 에 직접 연결
3. {type: 'join_room', pin: '4827'} 전송 → 서버가 무조건 수락
4. 고객 화면 WebRTC 스트림 수신 → **개인정보 유출**
5. 여기에 Phase 3 제어 권한이 더해지면:
- {type: 'request_control'} 전송
- 고객이 "관리자가 요청했다"고 착각하고 승인
- 공격자가 마우스/키보드 완전 제어 → **랜섬웨어 설치, 금융 정보 탈취 등**
sequenceDiagram
participant A as 관리자 브라우저
participant WP as WordPress (PHP)
participant SIG as Signal 서버
participant EXE as 고객 .exe
Note over A,WP: 페이지 로드 시
A->>WP: /remote-support/ 접속
WP->>WP: wr_is_admin() 검증
WP->>WP: 토큰 생성
{admin_id, exp, nonce} + HMAC-SHA256(WR_SECRET)
WP-->>A: 페이지에 토큰 주입 (JS 변수)
Note over A,SIG: WS 연결 시
A->>SIG: join_room {pin, token}
SIG->>SIG: 토큰 검증 (HMAC + 만료 + nonce)
alt 토큰 유효
SIG-->>A: peer_joined
SIG->>EXE: peer_joined {admin_name, admin_id}
EXE->>EXE: "김철수 관리자 연결됨" 표시
else 토큰 무효
SIG-->>A: error "인증 실패"
end
생성 (PHP, 플러그인 내):
function wr_generate_admin_token() {
if (!wr_is_admin()) return null;
$payload = [
'admin_id' => get_current_user_id(),
'admin_name' => wp_get_current_user()->display_name,
'exp' => time() + 300, // 5분 유효
'nonce' => wp_create_nonce('wr_admin_token'),
];
$payload_b64 = base64_encode(json_encode($payload));
$sig = hash_hmac('sha256', $payload_b64, WR_SECRET);
return $payload_b64 . '.' . $sig;
}
검증 (Node.js, signal/server.js에 추가):
const WR_SECRET = process.env.WR_SECRET; // docker-compose로 주입
function verifyAdminToken(token) {
if (!token || !WR_SECRET) return null;
const [payload_b64, sig] = token.split('.');
if (!payload_b64 || !sig) return null;
const expectedSig = crypto
.createHmac('sha256', WR_SECRET)
.update(payload_b64)
.digest('hex');
if (sig !== expectedSig) return null; // 서명 불일치
let payload;
try { payload = JSON.parse(Buffer.from(payload_b64, 'base64')); }
catch { return null; }
if (Date.now() / 1000 > payload.exp) return null; // 만료
return payload; // {admin_id, admin_name, exp, nonce}
}
// join_room 처리 시 (기존 코드 수정)
case 'join_room': {
const payload = verifyAdminToken(msg.token);
if (!payload) {
send(ws, { type: 'error', message: '관리자 인증 실패' });
ws.close(1008, 'unauthorized');
return;
}
ws._admin_id = payload.admin_id;
ws._admin_name = payload.admin_name;
// ... 기존 room 매칭 로직 ...
// 고객에게 admin_name 전달
send(room.host, { type: 'peer_joined', admin_name: payload.admin_name });
}
WR_SECRET 공유: docker-compose.yml에 환경변수로 동일한 값 주입 (WP와 signal 컨테이너 양쪽).
악성/버그 관리자가 입력 이벤트 폭탄으로 고객 .exe에 부하를 가하는 것을 방지:
| 이벤트 | 최대 주기 | 초과 시 |
|---|---|---|
mouse_move |
120/sec (8ms 간격) | 드롭 |
mouse_down/up |
30/sec | 드롭 |
mouse_wheel |
20/sec | 드롭 |
key_down/up |
30/sec | 드롭 |
고객 .exe에서 버퍼와 타임스탬프로 검사. 초과 분은 조용히 무시.
관리자 브라우저에서 다음 키 조합은 고객에게 전송하지 않고 브라우저가 처리하도록 허용:
| 키 | 이유 |
|---|---|
F5, Ctrl+R |
새로고침 (관리자가 의도한 것) |
Ctrl+W |
탭 닫기 |
Ctrl+T, Ctrl+N |
새 탭/창 |
Ctrl+Shift+T |
닫은 탭 복원 |
Alt+Tab |
OS 창 전환 (브라우저가 안 잡지만 전송 의미 없음) |
F11 |
전체화면 토글 |
Ctrl+L |
주소창 포커스 |
Ctrl+ +/-/0 |
브라우저 줌 |
구현: keydown 핸들러에서 이 키들은 e.preventDefault() 하지 않고 return.
다음 키는 고객에게 보내면 안 되는 위험한 키이므로 하드코트 차단:
| 키 | 이유 |
|---|---|
Ctrl+Alt+Del |
OS 레벨 (애초에 SendInput으로 안 됨) |
Win 단독 |
시작 메뉴 (고객 혼란) |
Alt+F4 |
고객 앱 강제 종료 위험 |
graph TD
L["exe 실행"] --> CONN["WSS 연결 중..."]
CONN --> WAIT["PIN 표시 대기 화면"]
WAIT --> STREAM["📺 화면 공유 중 + 관리자 대기"]
STREAM --> CTRL_REQ["⚠️ 관리자 제어 요청 모달"]
CTRL_REQ -->|"허용"| ACTIVE["🔴 제어 중 (빨간 테두리)"]
CTRL_REQ -->|"거절"| STREAM
ACTIVE -->|"관리자/고객 중지"| STREAM
STREAM --> END["종료"]
기존 관리자 화면에 다음 요소 추가:
<!-- 기존: 비디오 + 전체화면 + 연결종료 버튼 -->
<div class="wr-toolbar">
<button id="btnFullscreen">전체화면</button>
<!-- 신규 -->
<button id="btnControlToggle">🎮 원격 제어</button>
<button id="btnDisconnect">연결 종료</button>
</div>
<!-- 비디오 위 투명 오버레이 (제어 시 표시) -->
<div id="inputOverlay" class="wr-input-overlay">
<!-- 마우스 이벤트 캡처용 -->
<div id="remoteCursor"></div>
</div>
<!-- 상태 표시 추가 -->
<div class="wr-status">
<span class="wr-dot active"></span>
<span>시청 중</span>
<!-- 신규 -->
<span id="ctrlStatus" class="wr-ctrl-status">보기 전용</span>
</div>
| 상태 | 표시 |
|---|---|
| PIN 대기 중 | 파란 점 + "관리자 대기 중" |
| 시청 중 (보기만) | 초록 점 + "화면 공유 중" + 기본 창 |
| 제어 요청 수신 | 노란 점 + 깜빡임 + 승인 모달 |
| 제어 활성화 | 빨간 테두리 + "원격 제어 중" + 닫기 방지 확인 |
🚨 필수 스파이크. SIPSorcery가 비디오 + DataChannel 동시 지원하는지 먼저 검증한다.
실패 시 전체 계획 재검토 (Rust + libdatachannel 전환 등).
| 작업 | 상세 | 검증 기준 |
|---|---|---|
| SIPSorcery 동시 채널 스파이크 | 비디오 송출 + DataChannel 'test' 양방향 메시지 | 콘솔 앱에서 10초간 안정 동작 |
| H.264 하드웨어 인코딩 스파이크 | MediaFoundation으로 1080p30 인코딩 → RTP | CPU <15%, 30fps 유지 |
| SendInput 정확도 스파이크 | 메모장에 가상 키보드 입력, 마우스 클릭 | 100% 정확도 |
| GraphicsCapture 다중 모니터 스파이크 | 주 모니터 자동 캡처 | 부팅 직후 캡처 시작 |
| Go/No-Go 결정 | 위 스파이크 결과로 진행 여부 판단 | 4개 모두 PASS → 진행 |
실패 시 폴백:
libdatachannel + gfx-rs 전환 (개발 기간 +2주)SharpDX 또는 ScreenCaptureLib 래퍼 사용| 작업 | 상세 | 산출물 |
|---|---|---|
| 리포 초기화 | wp-wellcoms-remote-client GitLab 리포 생성 |
빈 리포 |
| C# 프로젝트 셋업 | .NET 8 WPF + SIPSorcery NuGet | 빌드 가능한 skeleton |
| WS 클라이언트 | 기존 시그널링 프로토콜 (create_room, sdp_offer 등) 그대로 구현 | 기존 signal 서버와 PIN 발급까지 연동 |
| 최소 UI | WPF 창 + PIN 4자리 표시 + 상태 라벨 | PIN 발급 표시 |
| 통합 테스트 | 기존 관리자 브라우저로 PIN 연결 가능한지 | 음성/화면 공유 없이 연결 성공 |
| 관리자 토큰 검증 추가 | signal/server.js에 verifyAdminToken() 추가 (§4.4) |
토큰 없는 join 차단 |
| WP 토큰 발급 | wr_generate_admin_token() PHP 함수, 페이지에 주입 |
관리자 페이지에서 토큰 사용 가능 |
| 작업 | 상세 | 산출물 |
|---|---|---|
| GraphicsCapture 통합 | Windows.Graphics.Capture API로 화면 캡처 | D3D11Texture 획득 |
| H.264 인코딩 | MediaFoundation 하드웨어 인코더 | RTP 패킷 송출 |
| WebRTC 비디오 트랙 | SIPSorcery RTP 송신 + SDP 협상 | 기존 브라우저에서 .exe 화면 보임 |
| DataChannel 'ctrl' 추가 | createDataChannel('ctrl') | P2P 메시지 채널 확보 |
| 통합 테스트 | 브라우저 관리자 → .exe 고객 화면 실시간 시청 | v1 마일스톤 (시청만) |
| 작업 | 상세 | 산출물 |
|---|---|---|
| DataChannel 'input' 추가 | 관리자→고객 입력 이벤트 채널 | 입력 메시지 수신 |
| SendInput P/Invoke | user32.dll 마우스/키보드 주입 | 입력 이벤트 실행 |
| 입력 메시지 디코딩 | JSON → INPUT 구조체 매핑 | mouse_move, key_down 등 처리 |
| 관리자 브라우저 입력 오버레이 | <video> 위 투명 div로 이벤트 캡처 |
관리자 마우스 → 고객 화면 |
| 좌표 변환 | 브라우저 비디오 크기 ↔ 원격 해상도 | 정확한 클릭 위치 |
| 통합 테스트 | 클릭, 드래그, 타이핑 E2E 동작 | v1 핵심 기능 완성 |
| 작업 | 상세 | 산출물 |
|---|---|---|
| 사용자 승인 모달 (.exe) | request_control 시 WPF 모달 | 명시적 허가 |
| 빨간 테두리 표시 (.exe) | 제어 중 창 테두리 + always-on-top | 시각적 경고 |
| 제어 중지 버튼 (.exe) | 고객이 1클릭으로 제어 철회 | 안전장치 |
| 관리자 토글 UX | "원격 제어" 버튼 활성/비활성 상태 | 직관적 토글 |
| 관리자 커서 표시 (옵션) | 고객 화면에 관리자 커서 위치 표시 | 피드백 |
| 세션 감사 로그 | DB 스키마 + 이벤트 로깅 | 감사 추적 |
| 예외 처리 | 연결 끊김, 입력 채널 오류, 캡처 실패 | 안정성 |
| 작업 | 상세 | 산출물 |
|---|---|---|
| 빌드 파이프라인 | Single-file publish + ZIP 묶음 | 배포 아티팩트 |
| PE 헤더 최적화 | 버전 정보, 회사명, 매니페스트 | AV false positive 완화 |
| VirusTotal 제출 | 주요 AV 통과 확인 | 배포 전 검증 |
| 워드프레스 다운로드 페이지 | /remote-support/에 다운로드 섹션 + 안내 |
사용자 진입점 |
| REST 버전 API | /wp-json/wr/v1/client/version |
향후 자동 업데이트용 |
| E2E 테스트 | 다양한 Windows 환경 (10/11, DPI, 멀티모니터) | 호환성 확보 |
| 내부 베타 | 실제 관리자가 사용해보며 피드백 | 런칭 준비 |
| 리스크 | 확률 | 영향 | 대응 |
|---|---|---|---|
| SIPSorcery WebRTC 안정성 | 중 | 높음 | 백업: Google WebRTC 네이티브 C++ 래퍼. 또는 Rust + libdatachannel 전환 |
| UAC 권한 앱 제어 불가 | 높음 | 중 | v1은 일반 앱만. v2에서 서비스 모드 추가 (관리자 권한으로 설치) |
| SmartScreen 경고로 사용자 포기 | 높음 | 높음 | 1단계: ZIP+안내. 2단계: Microsoft Trusted Signing ($10/월) |
| AV false positive (C# .NET) | 중 | 중 | PE 헤더 최적화, VirusTotal 모니터링, MS Security Intelligence 제보. 심하면 Rust 전환 |
| 화면 캡처 성능 부족 | 낮음 | 중 | H.264 하드웨어 인코딩 필수. 1080p 30fps 5Mbps 목표. 못하면 해상도/프레임 조정 |
| 좌표 정확도 (줌/전체화면) | 중 | 낮음 | object-fit contain 영역 계산 로직 검증. v2에서 캘리브레이션 모드 |
| 관리자 브라우저 입력 포커스 | 중 | 중 | 오버레이가 포커스 가로채도 기본 동작 차단 (preventDefault). F11 전체화면 권장 |
| 관리자 모바일 제어 불가 | 높음 | 낮음 | v1은 데스크탑만 명시. 모바일은 "시청만" 지원 (기존 기능 유지) |
| SIPSorcery 개발 중단 | 낮음 | 높음 | 백업 계획 문서화. WebRTC 네이티브 전환 로드맵 |
| 다중 모니터 캡처 | 중 | 낮음 | v1은 주 모니터만. 모니터 선택 UI는 v1.1 |
| 컴포넌트 | 테스트 항목 |
|---|---|
| .exe SendInput | 가상 마우스/키보드 이벤트 → 올바른 INPUT 구조체 생성 |
| .exe 좌표 변환 | 0..65535 정규화 로직 경계값 |
| .exe WS 클라이언트 | 재연결, 타임아웃, 잘못된 메시지 |
| 관리자 좌표 스케일 | 비디오 rect ↔ 원격 해상도 매핑 |
| 시나리오 | 검증 항목 |
|---|---|
| 정상 연결 | .exe 실행 → PIN → 관리자 연결 → 화면 표시 |
| 제어 활성화 | 토글 ON → 승인 → 마우스 이동 → 화면에서 커서 이동 |
| 제어 철회 | 고객 "제어 중지" → 즉시 입력 채널 닫힘 |
| 네트워크 끊김 | ICE disconnect → 1.5초 후 재연결 시도 |
| PIN 만료 | 30분 후 자동 종료 |
| 동시 세션 | 관리자 2명 동일 PIN → 거부 |
| 환경 | v1 목표 |
|---|---|
| Windows 11 (최신) | ✅ 완벽 지원 |
| Windows 10 22H2 | ✅ 완벽 지원 |
| Windows 10 1903+ | ✅ 지원 (GraphicsCapture 최소 요구) |
| Windows 8.1 / 7 | ❌ 미지원 |
| 관리자 Chrome 120+ | ✅ |
| 관리자 Edge 120+ | ✅ |
| 관리자 Firefox 115+ | ✅ |
| 관리자 Safari 17+ | ⚠️ 제한 (WebRTC DataChannel 안정성) |
| 관리자 모바일 | ❌ (시청만, 기존 기능) |
| 지표 | 목표 | 측정 |
|---|---|---|
| 지연 (입력 → 화면 반영) | < 100ms (P2P) / < 200ms (TURN) | chrome://webrtc-internals |
| 화면 프레임레이트 | 15-30fps (네트워크 적응) | 비디오 통계 |
| 대역폭 | 2-5 Mbps (1080p H.264) | 입력 카운터 |
| CPU (.exe) | < 15% (i5 기준) | 작업 관리자 |
| 메모리 (.exe) | < 200MB | 작업 관리자 |
| 증상 | 원인 | 대응 |
|---|---|---|
| PIN 발급 안 됨 | signal 서버 다운 | docker compose restart wellcoms_signal |
| TURN 릴레이 실패 | coturn 설정 오류 / 방화벽 | wellcoms_turn 로그, 3478 포트 확인 |
| 화면 안 보임 | WebRTC 협상 실패 | chrome://webrtc-internals, ICE 상태 확인 |
| 제어 안 됨 | DataChannel 닫힘 | 채널 상태 로그, 재연결 |
| exe 실행 불가 | .NET 런타임 / AV 차단 | .NET 8 설치 안내, AV 예외 추가 |
| 유형 | 방식 | 주기 |
|---|---|---|
| .exe 패치 | 다운로드 페이지 버전 갱신 (수동) | 필요 시 |
| .exe 자동 업데이트 | v1.1+ 에서 구현 | 주간 체크 |
| 플러그인 업데이트 | WP 관리자 페이지 | 필요 시 |
| 시그널링 서버 | Docker 이미지 rebuild | 필요 시 |
graph TD
V1["v1.0
원격 제어 기본
(이번 구현)"]
V11["v1.1
자동 업데이트
세션 녹화"]
V12["v1.2
파일 전송
클립보드 동기화"]
V13["v1.3
멀티 모니터
관리자 커서 시각화"]
V2["v2.0
UAC 권한 제어
(서비스 모드)"]
V3["v3.0
음성 통화
재부팅 후 재연결"]
V1 --> V11 --> V12 --> V13
V13 --> V2 --> V3
wp-content/plugins/wellcoms-remote/
├── wellcoms-remote.php ← v2.0.0 → v3.0.0 (입력 오버레이, 토글, 로깅 추가)
├── templates/
│ └── page-remote.php ← 다운로드 섹션 추가
├── signal/
│ ├── server.js ← 변경 없음 (옵션: 로깅 추가)
│ ├── package.json
│ └── Dockerfile
├── includes/ ← 신규
│ ├── class-wr-session-log.php ← 세션 로그 DB
│ └── class-wr-rest.php ← REST API (버전, 다운로드)
└── assets/ ← 신규 (입력 오버레이 CSS)
└── css/input-overlay.css
wp-wellcoms-remote-client/ ← GitLab 별도 리포
├── WellcomsRemote.sln ← Visual Studio 솔루션
├── WellcomsRemote/
│ ├── App.xaml / App.xaml.cs ← WPF 진입점
│ ├── MainWindow.xaml / .cs ← 메인 UI (PIN, 상태)
│ ├── ControlApprovalWindow.xaml / .cs ← 승인 모달
│ ├── Core/
│ │ ├── SignalingClient.cs ← WS 클라이언트 (기존 프로토콜)
│ │ ├── WebRtcPeer.cs ← SIPSorcery 래퍼
│ │ ├── ScreenCapturer.cs ← GraphicsCapture 통합
│ │ ├── H264Encoder.cs ← MediaFoundation 인코더
│ │ ├── InputInjector.cs ← SendInput P/Invoke
│ │ └── InputChannel.cs ← DataChannel 'input' 처리
│ ├── Models/
│ │ ├── InputEvent.cs ← JSON → C# 모델
│ │ └── SessionState.cs ← 상태 머신
│ └── docs/
│ └── README.md
├── WellcomsRemote.Tests/ ← xUnit 단위 테스트
└── scripts/
├── publish.ps1 ← Single-file publish 스크립트
└── sign.ps1 ← (향후) Trusted Signing 연동
wellcoms-remote-v1.0.0.zip ← GitHub Release / WP uploads
├── WellcomsRemote.exe ← 단일 파일 (~30-50MB)
├── 사용법.txt ← 한국어 안내
└── version.txt ← 버전 + SHA256
wr_is_admin() 체크와 무관하게 WS 직접 연결로 우회 가능. Phase 1(view only)에서는 감수 가능했으나 Phase 3(control)에서는 critical 취약점.웹화면뷰어-개발.md (Phase 1-2 완료 내역)웹화면뷰어-개발-공개.mddata/wp-content/plugins/wellcoms-remote/signal/server.js (관리자 토큰 검증 추가 — §3.3, §4.4 참조)gitlab.wellcoms.co.kr/jskim/wp-wellcoms-remote (기존)| 용어 | 정의 |
|---|---|
| WebRTC | 브라우저/앱 간 P2P 실시간 통신 규격. 비디오/오디오/데이터 전송 |
| DataChannel | WebRTC의 신뢰성 있는 양방향 메시지 채널 (TCP 유사) |
| ICE / STUN / TURN | NAT 통과를 위한 후보 수집/릴레이 프로토콜 |
| SDP / Offer / Answer | WebRTC 연결 협상 메시지 |
| SendInput | Windows API, 마우스/키보드 이벤트를 시스템 큐에 주입 |
| Windows.Graphics.Capture | Win10 1903+ 공식 화면 캡처 API |
| SmartScreen | Windows의 다운로드 파일 평판 검사 기능 |
| MOTW | Mark of the Web, 인터넷에서 다운로드된 파일에 붙는 메타데이터 |
| PE 헤더 | Windows 실행 파일 메타데이터 (버전, 회사명 등) |