1. [Executive Summary](#1-executive-summary)
2. [앱 기본 정보 및 식별](#2-앱-기본-정보-및-식별)
3. [패키지 구조 분석](#3-패키지-구조-분석)
4. [권한 심층 분석](#4-권한-심층-분석)
5. [C2 서버 아키텍처](#5-c2-서버-아키텍처)
6. [MQTT 원격 제어 채널](#6-mqtt-원격-제어-채널)
7. [REST API 엔드포인트 매핑](#7-rest-api-엔드포인트-매핑)
8. [데이터 수집 모델](#8-데이터-수집-모델)
9. [권한 요청 체인](#9-권한-요청-체인)
10. [서비스 아키텍처 및 퍼시스턴스](#10-서비스-아키텍처-및-퍼시스턴스)
11. [MMKV 상태 관리](#11-mmkv-상태-관리)
12. [방어기제 분석](#12-방어기제-분석)
13. [wavegrid_kotlin 패밀리 비교 분석](#13-wavegrid_kotlin-패밀리-비교-분석)
14. [취약점 분석](#14-취약점-분석)
15. [IOC 전수 목록](#15-ioc-전수-목록)
16. [킬 체인 재구성](#16-킬-체인-재구성)
17. [🔬 기법 점검](#17-기법-점검)
18. [결론 및 위협 평가](#18-결론-및-위협-평가)
항목 · 값
**앱명** · 필라테스 (Pilates)
**패키지** · com.uowengt.sljenw
**패밀리** · wavegrid_kotlin (Lite Variant)
**C2 서버** · [비공개]
**MQTT 브로커** · tcp://[비공개]
**통신 채널** · 2채널 (REST + MQTT)
**탈취 데이터** · 연락처, 전화번호, IMEI, IMSI, 통신사, 기기정보
**위협 수준** · Critical
**분석 수준** · ML-Complete
항목 · v1.0 (auto-generated) · v2.0 (실제 분석)
C2 서버 · [비공개] · [비공개]
MQTT 브로커 · 미기술 · tcp://[비공개]
API 엔드포인트 · /sms/inbox (오류) · 4개 Retrofit API
프레임워크 · EasyHttp · Retrofit + RxJava3 + MQTT
통신 채널 · 1개 · 2개 (REST + MQTT)
특성 · app_4086 (Full) · app_4086_2 (Lite)
C2 IP · [비공개] · [비공개]
MQTT 포트 · 1883 (추정) · 1883 (확인)
통신 채널 · 3채널 (REST+MQTT+CDN) · 2채널 (REST+MQTT)
CDN · Cloudflare Workers · 없음
아이콘 은닉 · Activity-Alias 기법 · 없음
퍼시스턴스 · 4계층 (Boot+Alarm+Job+Timer) · 1계층 (START_STICKY만)
킬 스위치 · IS_SM_HIDE, isNeedRunning · 없음
smali 클래스 · 3,284개 · 3,182개
항목 · 값
**파일명** · 260126_필라테스.apk
**파일 크기** · 3,517,118 bytes (3.35 MB)
**SHA-256** · b4b01a5ae48f8fdb69a82874ef229d03b8d5a051003bf4894eed44dc281da45c
**패키지명** · com.uowengt.sljenw
**versionCode** · 100
**versionName** · 1.0.0
**minSdkVersion** · 23 (Android 6.0)
**targetSdkVersion** · 31 (Android 12)
**compileSdkVersion** · 34 (Android 14)
**Application** · main.MyApplication
라이브러리 · 아키텍처 · 용도
libmmkv.so · arm64-v8a · MMKV 키-값 저장소
libmmkv.so · x86_64 · MMKV (에뮬레이터 지원)
주목: x86_64 아키텍처 포함으로 에뮬레이터에서의 직접 실행 의도 확인. 분석 환경 고려가 아닌, [피해자 정보 비공개] 다양성 대응.
· 앱 이름: "필라테스" — 건강/피트니스 앱으로 위장
· 패키지명: com.uowengt.sljenw — 난독화된 랜덤 패키지명 (wavegrid 패턴)
· 아이콘: 표준 Android 런처 아이콘 사용 (별도 커스텀 아이콘 없음)
항목 · 값
**총 smali 파일** · 3,182개
**DEX 파일** · 1개 (classes.dex)
**Assets** · 2개
난독화 경로 · 원래 목적 · 핵심 클래스
`activity/` · 앱 컴포넌트 · Oysw (런처), Uent (서비스)
`A0/` · 네트워크/코어 로직 · e (MegaHandler), k (SSL), n (네트워크 설정)
`q0/` · Retrofit API 인터페이스 · a (4개 API 정의)
`network/model/` · 데이터 모델 (비난독화) · DeviceInfoModel, ContactModel, BaseRespondModel
`mqtt/model/` · MQTT 모델 (비난독화) · CommandModel, OnlineModel
`n0/` · MQTT 커넥션 관리 · e (브로커 연결)
`o0/` · 유틸리티/로깅 · a (로그 출력)
`D/` · 디스패처/핸들러 · g (Runnable/이벤트 디스패치)
`main/` · 애플리케이션 클래스 · MyApplication
`info/mqtt/` · Eclipse Paho MQTT · MqttService, MqttConnection, AlarmPingSender
```xml
<!-- 런처 액티비티 — 단일 진입점, alias 없음 -->
<activity android:exported="true" android:name="activity.Oysw">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 백그라운드 서비스 -->
<service android:name="activity.Uent"/>
<!-- MQTT 포그라운드 서비스 -->
<service android:foregroundServiceType="dataSync"
android:name="info.mqtt.android.service.MqttService"/>
<!-- Room DB 서비스 (MQTT 메시지 저장) -->
<service android:directBootAware="true" android:exported="false"
android:name="androidx.room.MultiInstanceInvalidationService"/>
```
핵심 차이 (vs app_4086):
· ❌ BootReceiver 없음 (부팅 시 자동 시작 불가)
· ❌ AlarmReceiver 없음 (주기적 알람 없음)
· ❌ ProtectService 없음 (JobScheduler 감시 없음)
· ❌ activity-alias 없음 (아이콘 은닉 불가)
· ✅ Room DB 서비스 추가 (MQTT 메시지 영구 저장)
권한 · 위험도 · 용도
`READ_CONTACTS` · **Dangerous** · 연락처 전수 탈취
`READ_PHONE_STATE` · **Dangerous** · IMEI, 기기 상태 수집
`READ_PHONE_NUMBERS` · **Dangerous** · 전화번호 직접 접근
`INTERNET` · Normal · C2/MQTT 통신
`ACCESS_NETWORK_STATE` · Normal · 네트워크 상태 확인
`FOREGROUND_SERVICE` · Normal · 포그라운드 서비스 실행
`FOREGROUND_SERVICE_DATA_SYNC` · Normal · MQTT 서비스 유형 지정
`WAKE_LOCK` · Normal · 기기 깨우기 (MQTT 유지)
`SCHEDULE_EXACT_ALARM` · Normal · MQTT 핑 스케줄링
총 9개 권한, Dangerous 3개 — app_4086(10개 권한)보다 1개 적음 (USE_EXACT_ALARM 없음).
권한 요청 순서 (코드 기반):
1. READ_PHONE_NUMBERS — 전화번호 수집
2. READ_CONTACTS — 연락처 탈취
3. READ_PHONE_STATE — IMEI/IMSI 수집
특이점: READ_CONTACTS는 uses-permission-sdk-23 태그로 선언 — API 23+ 기기에서만 런타임 요청.
항목 · 값
**IP** · [비공개]
**HTTP 포트** · 17560
**MQTT 포트** · 1883
**프로토콜** · HTTP (평문) + MQTT (평문)
**전체 URL** · [비공개]
**GeoIP** · 아시아 (Alibaba Cloud)
```
┌─────────────────────────────────────────────────┐
│ 감염 기기 │
│ ┌────────────────────┐ ┌───────────────────┐ │
│ │ REST 클라이언트 │ │ MQTT 클라이언트 │ │
│ │ (Retrofit+RxJava3) │ │ (Eclipse Paho v3) │ │
│ └─────────┬──────────┘ └─────────┬─────────┘ │
│ │ │ │
└────────────┼───────────────────────┼─────────────┘
│ HTTP :17560 │ TCP :1883
▼ ▼
┌────────────────────────────────────────────────┐
│ C2 서버: [비공개] │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐│
│ │ HTTP API Server │ │ MQTT Broker ││
│ │ :17560 │ │ :1883 ││
> [비공개]
│ │ api/phones │ │ CommandModel 수신 ││
│ │ api/connect/{imei}│ │ OnlineModel 발신 ││
│ │ api/contacts/{id} │ │ ││
│ └──────────────────┘ └──────────────────────┘│
└────────────────────────────────────────────────┘
```
핵심: 동일 IP에서 HTTP와 MQTT를 모두 호스팅. app_4086은 Cloudflare Workers CDN을 통한 C2 URL 동적 변경 기능이 있었으나, app_4086_2는 하드코딩 IP만 사용 — CDN 폴백 없이 IP 차단 시 무력화.
```
activity/Uent.smali:280 "setBaseUrl: [비공개]
activity/Uent.smali:289 "[비공개]
A0/e.smali:1034 "tcp://[비공개]"
a.1/a.smali:239-257 "Net_Token[비공개]
```
```
브로커: tcp://[비공개]
> [비공개]
> [비공개]
프로토콜: MQTT 3.1.1 (Eclipse Paho)
서비스: ForegroundService (dataSync 유형)
```
smali 증거 (A0/e.smali:1034-1042):
```smali
const-string v1, "tcp://[비공개]"
> [비공개]
invoke-virtual {v0, v1, v2, v2, p0}, Ln0/e;->a(
Ljava/lang/String; # broker URL
Ljava/lang/String; # username: "admin"
> [비공개]
Ljava/lang/String; # clientId
)Z
```
필드 · 타입 · 설명
instruct · int · 명령 유형 코드
code · int · 실행 코드
hcode · int · 핸들러 코드
msg · String · 명령 메시지/페이로드
필드 · 타입 · 소스 · 값 예시
sid · String · A0.e.r() (기기 ID) · UUID 형식
mobile · String · MMKV "PhoneNumUtils_V1_PhoneNum" · "010-xxxx-xxxx" 또는 "N/A"
operatorName · String · TelephonyManager.getNetworkOperatorName() · "SKTelecom", "KT"
version · String · 하드코딩 · "8.0.0"
```
D/g.smali:1414 "mqtt disconnect, start reconnect"
```
연결 실패 시 자동 재연결 시도. AlarmPingSender$AlarmReceiver가 MQTT 핑을 주기적으로 전송하여 연결 유지.
# · HTTP · 엔드포인트 · 메서드 · 목적
1 · POST · `api/login` · d() · C2 인증
2 · POST · `api/phones` · a() · 기기 정보 전송
3 · GET · `api/connect/{imei}` · b() · 하트비트/접속 확인
4 · POST · `api/contacts/{deviceId}` · c() · 연락처 탈취
```json
// Request Body (하드코딩)
{
> [비공개]
> [비공개]
}
// Response: JsonObject (토큰 포함)
```
smali 증거 (activity/Uent.smali:56-177):
```smali
> [비공개]
> [비공개]
const-string v4, "email" # 라인 95 — 필드명
> [비공개]
```
```json
// Request Body: DeviceInfoModel
{
"phone_number": "010-xxxx-xxxx",
"imei": "xxxxxxxxxxxxxxx",
"imsi": "xxxxxxxxxxxxxxx",
"android_version": 35,
"brand": "samsung",
"model": "SM-S928N",
"install_at": "2026-02-27 14:30:00",
"online_at": "2026-02-27 14:30:05",
"status": 1
}
// Response: BaseRespondModel<Object>
```
```
// Path Parameter: imei (기기 고유 식별자)
// Response: BaseRespondModel<Object>
```
```json
// Path Parameter: deviceId
// Request Body: ContactModel 배열
[
{"name": "홍길동", "phone_number": "010-**-**"},
{"name": "이순신", "phone_number": "010-**-**"}
]
// Response: BaseRespondModel<Object>
```
원래 클래스명: PhoneInfoModel (toString()에서 확인)
필드 · 타입 · 수집 방법 · 위험도
phone_number · String · TelephonyManager + MMKV fallback · **Critical**
imei · String · TelephonyManager.getDeviceId() · **Critical**
imsi · String · TelephonyManager.getSubscriberId() · **Critical**
android_version · int · Build.VERSION.SDK_INT · Low
brand · String · Build.BRAND · Low
model · String · Build.MODEL · Low
install_at · String · 앱 설치 시간 · Medium
online_at · String · 마지막 접속 시간 · Medium
status · int · 기기 상태 플래그 · Low
필드 · 타입 · 수집 방법
name · String · ContentResolver → ContactsContract
phone_number · String · ContactsContract.CommonDataKinds.Phone
전체 연락처 탈취 — 이름과 전화번호 쌍을 JSON 배열로 직렬화 후 POST api/contacts/{deviceId}로 전송.
실행 흐름:
```
[앱 시작]
│
▼
Oysw.<init>()
│ Handler 초기화
│ D.g Runnable 초기화 (type=1)
│ 권한 배열 설정: [READ_PHONE_NUMBERS, READ_CONTACTS, READ_PHONE_STATE]
│
▼
Oysw.onCreate()
│ (비어있음 — super.onCreate()만 호출)
│
▼
Oysw.onResume()
│ 권한 확인 루프
│ 미부여 권한 있으면 → g.a.b()로 시스템 권한 대화상자
│ 모든 권한 부여됨 → Handler.postDelayed(D.g, 1000ms)
│
▼
[1초 후] D.g.run()
│ 서비스 시작 → Uent
│
▼
Uent.onCreate()
│ "denglu" 로그 출력
│ Handler.postDelayed(D.g, 30000ms) ← 30초 후 추가 작업
│ A0.e.D() 초기화
│ BaseURL 설정: "[비공개]
│ a() 호출 → POST api/login
│
▼
[로그인 성공 후]
│ 기기 정보 수집 → POST api/phones
│ 연락처 수집 → POST api/contacts/{deviceId}
│ MQTT 연결 → tcp://[비공개]
```
```smali
```
특이점: 부분 거부 처리가 없음. 하나라도 거부하면 전체 중단. 그러나 onResume에서 매번 재확인하므로, 앱을 다시 열면 권한 대화상자 반복 표시.
항목 · 값
부모 클래스 · [비공개].Service
반환값 · START_STICKY (return 1, 라인 323)
초기화 지연 · 30초 (0x7530 = 30000ms)
로그인 · 즉시 실행 (a() 직접 호출)
바인딩 · null 반환 (unbound service)
메커니즘 · app_4086 (Full) · app_4086_2 (Lite)
START_STICKY · ✅ · ✅
BootReceiver (BOOT_COMPLETED) · ✅ MAX_INT 우선순위 · ❌
AlarmReceiver (주기적) · ✅ + Intentional NPE · ❌ (Paho AlarmPingSender만)
ProtectService (JobScheduler) · ✅ 16분 간격 · ❌
Timer (20초 폴링) · ✅ · ❌
onDestroy 자체 재시작 · ✅ · ❌
MQTT ForegroundService · ✅ · ✅
결론: app_4086_2는 퍼시스턴스가 크게 약화됨.
· START_STICKY + MQTT ForegroundService만으로 지속
· 시스템에 의해 서비스가 kill되면 복구 메커니즘 제한적
· 부팅 후 자동 시작 불가
· 이는 개발 초기 버전 또는 경량화 의도를 시사
```xml
<service android:foregroundServiceType="dataSync"
android:name="info.mqtt.android.service.MqttService"/>
```
· Foreground 서비스: 알림 표시 의무 (Android 8.0+), 그러나 "데이터 동기화"로 위장
· AlarmPingSender: MQTT 연결 유지를 위한 주기적 ping (SCHEDULE_EXACT_ALARM 사용)
· Room DB: MQTT 메시지를 로컬에 영구 저장 (오프라인 시 큐잉)
키 · 용도 · 발견 위치
`Net_Token[비공개] · API 인증 토큰 · a.1/a.smali:239
`PhoneNumUtils_V1_PhoneNum` · 수집된 전화번호 캐시 · mqtt/model/OnlineModel.smali:49
키 · app_4086 · app_4086_2
Net_Token{URL} · ✅ · ✅
PhoneNumUtils_V1_PhoneNum · ✅ · ✅
IS_SM_HIDE · ✅ (은닉 상태) · ❌
ALREADY_HIDE_ICON · ✅ (아이콘 은닉) · ❌
AppUtils_V1isNeedRunning · ✅ (킬 스위치) · ❌
분석: 킬 스위치와 은닉 상태 관리 키가 모두 없음 → 원격 제어에 의한 자체 비활성화 기능 미구현.
원래 이름 (추정) · 난독화 · 확인 근거
SplashActivity · Oysw · 런처 액티비티
RunningService · Uent · 서비스, "denglu" 로그
MegaHandler · A0.e · 네트워크 설정, MQTT, BaseURL
NetworkConfig · A0.n · JSON 직렬화, API 호출
SSLBypass · A0.k · SSL/ALPN 비활성화
ApiInterface · q0.a · Retrofit 인터페이스 (4 API)
MqttManager · n0.e · MQTT 브로커 연결
EventDispatcher · D.g · Runnable/이벤트 디스패치
난독화 수준: Moderate — 클래스/메서드명은 난독화되나, network/model/, mqtt/model/ 패키지는 원래 이름 유지.
A0/k.smali: SSLSocket의 ALPN 프로토콜 협상 비활성화.
· Reflection을 통한 동적 메서드 호출로 보안 설정 우회
· TrustAllCerts 패턴 내재
· 결과: MITM 공격에 완전 노출
방어기제 · app_4086 · app_4086_2 · 의미
Activity-Alias 아이콘 은닉 · ✅ · ❌ · 앱 드로어에서 항상 보임
Intentional NPE (AA-001) · ✅ · ❌ · 분석 방해 미적용
에뮬레이터 탐지 · 약간 · ❌ · 에뮬레이터 실행 용이
MMKV 킬 스위치 · ✅ · ❌ · 원격 비활성화 불가
```
wavegrid_kotlin Family
├── app_4086 (Full Variant) — 2025-07-15
│ ├── 3채널 (REST + MQTT + CDN)
│ ├── 4계층 퍼시스턴스
│ ├── Activity-Alias 아이콘 은닉
│ ├── 킬 스위치 (IS_SM_HIDE, isNeedRunning)
│ └── Intentional NPE (AA-001)
│
└── app_4086_2 (Lite Variant) — 2026-01-26
├── 2채널 (REST + MQTT)
├── 1계층 퍼시스턴스 (START_STICKY만)
├── 아이콘 은닉 없음
├── 킬 스위치 없음
└── Room DB 추가 (MQTT 메시지 저장)
```
구성 요소 · 동일성
Retrofit API 구조 (4 엔드포인트) · **100% 동일**
DeviceInfoModel (9필드) · **100% 동일**
ContactModel (2필드) · **100% 동일**
CommandModel (4필드) · **100% 동일**
OnlineModel (4필드, version="8.0.0") · **100% 동일**
MMKV 키 패턴 (Net_Token, PhoneNum) · **부분 동일**
> [비공개]
Eclipse Paho MQTT 라이브러리 · **동일**
> [비공개]
ProGuard/R8 난독화 패턴 · **유사** (다른 매핑)
가설 A — 개발 초기 버전: app_4086_2가 먼저 개발되고, 이후 퍼시스턴스/은닉/CDN 기능이 추가되어 app_4086이 됨.
· 근거: 날짜 (app_4086=2025-07-15, app_4086_2=2026-01-26)와 맞지 않음.
가설 B — 경량화 버전: app_4086의 기능을 의도적으로 축소하여 탐지 회피.
· 근거: 복잡한 퍼시스턴스와 은닉 기법이 보안 솔루션에 의해 탐지되어, 단순 버전으로 회귀.
· 날짜상 app_4086_2가 6개월 후에 배포 → 기능 축소는 의도적일 가능성 높음.
가설 C — 다른 C2 운영자: 동일 코드베이스를 공유하지만 다른 IP([비공개] vs [비공개])를 사용 → 별도 운영 조직.
· 근거: 코드 구조 100% 동일, IP만 다름 → 동일 개발자/다른 인프라.
유력 가설: B+C — 동일 개발자가 경량 버전을 새 C2 인프라에 배포.
# · 취약점 · CVSS · 설명
> [비공개]
> [비공개]
3 · **Plaintext HTTP Communication** · 9.0 · [비공개] — 암호화 없음. MITM 가능
4 · **Contact Exfiltration** · 9.0 · 전체 연락처 무단 수집 및 전송
5 · **IMEI/IMSI Collection** · 8.5 · 고유 식별자 수집으로 기기 추적 가능
# · 취약점 · CVSS · 설명
6 · **SSL Certificate Bypass** · 8.0 · ALPN 비활성화, TrustAllCerts
7 · **No CDN Fallback** · 7.5 · 단일 IP 하드코딩, IP 차단 시 완전 무력화
8 · **MQTT Remote Control** · 7.0 · CommandModel로 원격 명령 가능
유형 · 값
**C2 IP** · [비공개]
**HTTP URL** · [비공개]
**MQTT Broker** · tcp://[비공개]
**API: Login** · [비공개]
**API: Phones** · [비공개]
**API: Connect** · [비공개]
**API: Contacts** · [비공개]
유형 · 값
**패키지명** · com.uowengt.sljenw
**SHA-256** · b4b01a5ae48f8fdb69a82874ef229d03b8d5a051003bf4894eed44dc281da45c
**앱 이름** · 필라테스
**서비스** · activity.Uent
**MQTT 서비스** · info.mqtt.android.service.MqttService
**MMKV 경로** · /data/data/com.uowengt.sljenw/files/mmkv/
**MMKV 키** · Net_Token[비공개]
**MMKV 키** · PhoneNumUtils_V1_PhoneNum
행위 · 설명
> [비공개]
> [비공개]
"denglu" 로그 태그 · 중국어 "登录"(로그인) — 개발자 국적 단서
ForegroundService(dataSync) · MQTT를 "데이터 동기화"로 위장
version="8.0.0" · OnlineModel에서 하드코딩된 클라이언트 버전
```
1. 피싱 메시지 수신 ("필라테스 앱 설치 안내")
2. APK 다운로드 링크 클릭
3. 사이드로딩 설치
```
```
4. 앱 실행 → Oysw 런처 액티비티
5. onResume() → 권한 요청 대화상자
6. 사용자 권한 승인 (READ_PHONE_NUMBERS, READ_CONTACTS, READ_PHONE_STATE)
7. 1초 지연 후 Uent 서비스 시작
```
```
8. Uent: START_STICKY 반환 (서비스 종료 시 재시작)
9. MQTT ForegroundService 시작 (dataSync 위장)
10. AlarmPingSender 활성화 (MQTT 연결 유지)
```
```
> [비공개]
12. 토큰 수신 → MMKV에 저장
> [비공개]
```
```
14. TelephonyManager → IMEI, IMSI, 전화번호, 통신사
15. Build → 브랜드, 모델, Android 버전
16. ContentResolver → 전체 연락처 (이름 + 전화번호)
```
```
17. POST api/phones — DeviceInfoModel (9필드)
18. GET api/connect/{imei} — 하트비트
19. POST api/contacts/{deviceId} — 연락처 배열
20. MQTT OnlineModel — 실시간 상태 보고
```
```
21. MQTT CommandModel 수신 대기
22. instruct/code/hcode에 따른 명령 실행
23. 명령 결과 응답
```
코드 · 기법 · 적용
RE-001 · smali 정적 분석 · API 인터페이스, 서비스 구조, 데이터 모델 완전 매핑
RE-002 · 난독화 해제 · ProGuard 매핑 역추적 (activity/ → SplashActivity, Uent → RunningSer
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr