Analyst: ArkLink Security Research
Date: 2026-03-03
Report Version: v1.0
App Version: 1.1.33 (versionCode: 10101)
SHA256: 1397b54ef8f274c61a105539b7389b0f75cca4e28d2754d80eb21a5a63919c94
Whale은 wavegrid_gen3 패밀리에 속하는 악성 데이터 탈취 앱으로, 초대코드 기반 접근 제어를 사용한다. 앱은 사용자의 SMS, 연락처, 사진을 수집하여 `[비공개] C2 서버로 전송한다. 특히 UploadService를 통한 5초 주기 SMS 변경 감지 메커니즘이 주목할 만하며, 이는 실시간 SMS 인증번호 탈취를 위한 설계로 판단된다.
위협 수준: EXTREME
패밀리: wavegrid_gen3
프레임워크: Native Android (R8 Heavy Obfuscation)
```
com.Whalemsg.JINUH (패키지명)
com.example.mydemo (실제 코드 패키지 — 개발 템플릿 흔적)
├── BaseApp.java — Application 클래스
├── LoginActivity.java — 초대코드 입력 + 최초 실행 분기
├── UploadActivity.java — 핵심 데이터 탈취 오케스트레이터
│ ├── $a ~ $d — 4개 버튼 클릭 리스너
│ ├── $e — Luban 압축 콜백 (OnCompressListener)
│ ├── $f — 연락처 업로드 Thread (Runnable)
│ ├── $f$a — 연락처 업로드 OnSuccess 콜백
│ ├── $g — SMS 업로드 Thread (Runnable)
│ └── $g$a — SMS 업로드 OnSuccess 콜백
├── ContactActivity.java — (보조 UI)
├── AlbumActivity.java — (보조 UI)
├── MainActivity.java — (미사용 또는 보조)
├── b.java — 사진 업로드 Runnable (multipart)
│ └── $a — 업로드 결과 콜백
├── bean/
│ ├── ContactBean.java — 연락처 데이터 모델 (name, number)
│ ├── SmsBean.java — 다목적 데이터 모델 (SMS + 사진 + 연락처)
│ └── OnSuccess.java — 네트워크 콜백 인터페이스
├── servise/ (오타: service)
│ ├── UploadService.java — 백그라운드 SMS 폴링 서비스
│ ├── $1 — Handler
│ ├── $2 — CountDownTimer (5초 주기)
│ └── $3 — SMS 업로드 Thread
└── (유틸리티 — R8 난독화)
├── q2/c.java — HTTP 클라이언트 (POST JSON)
├── q2/d.java — 데이터 수집 유틸리티
├── q2/e.java — SharedPreferences 유틸리티
├── c1/a.java — Gson JSON 직렬화
├── k6/h.java — Luban 이미지 압축
├── t3/k.java — Gson 설정 (serializeNulls=false)
└── y5/*.java — OkHttp3 네트워크 스택
```
파일 · 아키텍처 · 용도
`lib/arm64-v8a/libscannative.so` · ARM64 · Huawei ScanKit (QR 스캔)
`lib/armeabi-v7a/libscannative.so` · ARMv7 · Huawei ScanKit (QR 스캔)
> ScanKit은 실제 기능이 아닌 번들 라이브러리로, 앱 위장 목적으로 포함된 것으로 추정.
권한 · 수집 대상 · 위험도
`READ_SMS` · SMS 메시지 전체 · CRITICAL
`SEND_SMS` · SMS 발송 능력 · CRITICAL
`READ_CONTACTS` · 연락처 전체 · HIGH
`READ_PHONE_NUMBERS` · 전화번호 · HIGH
`READ_PHONE_STATE` · 기기 식별자 · MEDIUM
`READ_EXTERNAL_STORAGE` · 사진/파일 · HIGH
`READ_MEDIA_IMAGES` · 사진 (API 33+) · HIGH
`READ_MEDIA_VIDEO` · 동영상 (API 33+) · MEDIUM
`READ_MEDIA_AUDIO` · 오디오 (API 33+) · LOW
`INTERNET` · 네트워크 통신 · -
`ACCESS_NETWORK_STATE` · 네트워크 상태 · -
> SEND_SMS 권한: SMS 수신만이 아닌 발송 능력까지 보유. 스미싱 발송 벡터로 악용 가능.
```
[앱 실행]
→ BaseApp.onCreate()
→ registerActivityLifecycleCallbacks(BaseApp$a)
→ LoginActivity.onCreate()
→ SharedPreferences "is_first" 확인
→ true (최초 실행):
→ Intent → UploadActivity 시작
→ finish()
→ false (재실행):
→ 로그인 UI 표시 (EditText: 초대코드, TextView: 로그인 버튼)
→ UUID 생성/로드 (SharedPreferences)
```
```
[UploadActivity.onCreate()]
→ UploadService.startService() ← 백그라운드 SMS 폴링 시작
→ SharedPreferences에서 invite_code 로드
→ 4개 버튼 설정 ($a, $b, $c, $d)
→ "is_upload" 플래그 확인
→ false: w() 호출 → 6종 권한 요청
→ true: 이미 업로드 완료
[w() — 권한 요청]
→ requestPermissions([
READ_SMS, SEND_SMS,
READ_PHONE_NUMBERS, READ_PHONE_STATE,
READ_CONTACTS, READ_EXTERNAL_STORAGE
])
[onRequestPermissionsResult()]
→ 모든 권한 허용:
→ y() 호출 (데이터 수집 + 업로드 시작)
→ 권한 거부:
→ x(I) 호출 → AlertDialog → 앱 종료
[y() — 수집 + 업로드 오케스트레이션]
→ TelephonyManager.getLine1Number() → 전화번호 획득
→ v() — SMS 수집 + 업로드
→ u() — 연락처 수집 + 업로드
→ t() — 사진 수집 + 압축 + 업로드
[업로드 완료]
→ s() — "알 수 없는 오류로 인해 애플리케이션이 종료되었습니다" 표시
→ 사용자에게 앱 충돌로 위장
```
```
[UploadService.onCreate()]
→ getLine1Number() → 전화번호 저장
→ countDown() → CountDownTimer(5000ms, 1000ms) 시작
[CountDownTimer.onFinish() — 5초마다]
→ READ_SMS 권한 확인
→ q2/d.c() → 현재 SMS 목록 수집
→ 현재 SMS 수 vs 저장된 SMS 수 비교
→ 불일치 (새 SMS 도착):
→ is_sync = true
→ getSmsDataUpload() → POST [비공개]
→ 일치 (변화 없음):
→ is_sync = false
→ getSmsDataUpload() 호출 (빈 리스트 전송)
→ countDown() 재호출 → 무한 루프 (재귀적 타이머 생성)
[getSmsDataUpload()]
→ 전화번호 재수집 (TelephonyManager)
→ SMS 목록 수집 (q2/d.c())
→ invite_code → userName
→ phoneNumber → bjPhone
→ is_sync ? smsList : emptyList → JSON 직렬화 → content
→ Thread(UploadService$3) → POST webapp/saveSms
[UploadService.onStartCommand()]
→ return START_STICKY (1) — 시스템이 서비스 자동 재시작
[UploadService.onDestroy()]
→ new Intent(this, UploadService.class) — 자기 자신 재시작 준비
→ (실제 startService 호출은 빠져있으나 Intent 생성 시도)
```
항목 · 값
IP · [비공개]
포트 · 80 (HTTP)
프로토콜 · HTTP (평문)
Base URL · `[비공개]
서버 프레임워크 · RuoYi (prod-api 경로 패턴)
하드코딩 위치 · `b.smali:212`, `q2/c.smali:82`
```smali
const-string v6, "[비공개]
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(...)
invoke-virtual {v5, v0}, Ljava/lang/StringBuilder;->append(...)
const-string v5, "[비공개]
```
# · Method · Endpoint · Purpose · Caller · Content-Type
1 · POST · `[비공개] · SMS 데이터 전송 · UploadActivity$g, UploadService$3 · application/json
2 · POST · `[비공개] · 연락처 전송 · UploadActivity$f · application/json
3 · POST · `[비공개] · 사진 업로드 · b.smali (Runnable) · multipart/form-data
4 · POST · `[비공개] · 로그인/인증 · LoginActivity · application/json
```
q2/c.a() — 동기 HTTP POST:
→ OkHttp3 Request 생성
→ 동기 실행 (w.c() → b0.a() → 응답)
→ HTTP 200-299 성공 판정 (0xc8 ≤ code < 0x12c)
→ 응답 JSON 파싱: { "code": 200, ... }
→ OnSuccess.onSuccess(code, responseBody)
→ 로그 출력: "返回=" + response (중국어 디버그)
```
```
데이터 소스: content://sms/
수집 컬럼: address, date, type, body
저장 구조: ArrayList<SmsBean>
SmsBean.displayName = address (발신/수신 번호)
SmsBean.date = date (타임스탬프)
SmsBean.type = type (1=수신, 2=발신)
SmsBean.content = body (메시지 내용)
필터링: 없음 (전체 SMS 수집)
정렬: 없음 (기본 순서)
디버그 로그: Log.e("TAG", "address:${address},date:${date},type:${type},body:${body}")
```
```
데이터 소스: ContactsContract.CommonDataKinds.Phone.CONTENT_URI
수집 컬럼: display_name, data1, contact_id
저장 구조: ArrayList<SmsBean> (SmsBean 재사용!)
SmsBean.displayName = display_name (연락처 이름)
SmsBean.content = data1 (전화번호)
필터링: 없음 (전체 연락처)
디버그 로그: Log.d("TAG", "getContact===" + name/phone/id)
```
```
데이터 소스: MediaStore.Images.Media.EXTERNAL_CONTENT_URI
수집 컬럼: _id, _display_name, _data
저장 구조: ArrayList<SmsBean> (SmsBean 재사용!)
SmsBean.path = _data (파일 경로)
필터링: 없음 (전체 사진)
압축: Luban (k6/h) → 콜백 UploadActivity$e → b.smali 업로드
```
> SmsBean 다목적 사용: 동일한 Bean 클래스를 SMS, 연락처, 사진에 모두 사용. 낮은 코드 품질의 전형.
```
소스: TelephonyManager.getLine1Number()
호출 위치:
· UploadActivity.y() — 메인 수집
· UploadService.onCreate() — 서비스 시작 시
· UploadService.getSmsDataUpload() — SMS 전송 시 매번
예외 처리: @SuppressLint("MissingPermission"), try-catch 무시
```
```
q2/c.a(endpoint, params, callback)
└── Gson (t3/k → t3/j)
└── serializeNulls=false
└── toJson(Map) → JSON String
└── OkHttp3 (y5 패키지)
├── y5/u — OkHttpClient
├── y5/x — Request
├── y5/x$a — Request.Builder
├── y5/w — Call
├── y5/b0 — Response
├── y5/d0 — ResponseBody
├── y5/s — MediaType
├── y5/y — RequestBody (JSON)
├── y5/z — RequestBody (File)
└── y5/t — MultipartBody
사진 업로드 (b.smali):
MultipartBody.Builder
├── Part: "file" = {filename, "image/*", File}
└── Part: "other_field" = "value" (더미 필드)
```
항목 · 상태 · 위험도
TLS/HTTPS · 미사용 (HTTP 평문) · CRITICAL
데이터 암호화 · 없음 (JSON 평문) · HIGH
Certificate Pinning · 없음 · HIGH
SSL Bypass · auto_analysis에서 탐지됨 · CRITICAL
디버그 로깅 · Log.d/Log.e 활성 · MEDIUM
기법 · 구현 · 효과
R8 Obfuscation · 단일 문자 클래스명 (q2, c1, k6, y5, t3 등) · 분석 난이도 증가
Package Mismatch · 선언: com.Whalemsg.JINUH, 실제: com.example.mydemo · 분석자 혼란
Debug Markers · "tets", "222222222", "11111111" · 개발 흔적 노출
Huawei SDK Bundle · ScanKit, AGConnect, ML Kit · 합법 앱 위장
기법 · 구현
START_STICKY · UploadService.onStartCommand() returns 1
Self-Restart Intent · UploadService.onDestroy() → new Intent(UploadService.class)
Recursive Timer · CountDownTimer.onFinish() → countDown() 재호출
is_first 플래그 · 최초 실행 후 LoginActivity 우회 → 직접 UploadActivity
is_upload 플래그 · 업로드 완료 후 재수집 방지 (그러나 UploadService는 계속 폴링)
기법 · 설명
초대코드 · [피해자 정보 비공개] + 접근 제어
앱 충돌 위장 · 업로드 완료 후 "알 수 없는 오류" 다이얼로그 표시
비디오 배경 · FullScreenVideoView로 합법 앱 분위기 조성 (auto_analysis.json에서 확인)
6종 권한 일괄 요청 · 한 번에 모든 권한을 요청하여 거부 기회 최소화
ID · 취약점 · 심각도 · 근거
VULN-001 · Cleartext HTTP · CRITICAL · C2 `[비공개] — 전송 중 데이터 노출
VULN-002 · SSL Certificate Bypass · CRITICAL · auto_analysis.json: ssl_bypass=true
VULN-003 · No Payload Encryption · HIGH · Gson.toJson() 평문 JSON 전송
VULN-004 · Debug Logging Active · MEDIUM · Log.d("TAG", ...), Log.e("TAG", ...) — 민감 데이터 logcat 노출
VULN-005 · Exported Service · MEDIUM · UploadService exported=true — 외부 앱에서 시작 가능
VULN-006 · Hardcoded C2 IP · MEDIUM · b.smali:212, q2/c.smali:82
VULN-007 · Missing Permission Check · LOW · @SuppressLint("MissingPermission") — 권한 확인 무시
VULN-008 · Debug Strings · LOW · "tets", "222222222", "11111111" — 개발자 흔적
유형 · 값
IP · [비공개]
URL · [비공개]
URL · [비공개]
URL · [비공개]
URL · [비공개]
유형 · 값
SHA256 · 1397b54ef8f274c61a105539b7389b0f75cca4e28d2754d80eb21a5a63919c94
Package · com.Whalemsg.JINUH
Internal Package · com.example.mydemo
App Label · Whale
Version · 1.1.33 (10101)
유형 · 값
Service · com.example.mydemo.servise.UploadService
API Pattern · [비공개]
Debug Tag · "TAG", "tets"
Debug Values · "222222222", "11111111"
Chinese Strings · "返回=" (response log)
Typo · "servise" (service)
SharedPreferences · "is_first", "is_upload", "invite_code"
특성 · Whale · zbdx (참조)
C2 IP · [비공개] · [비공개]
API 패턴 · [비공개] · /app/upload/*
HTTP 클라이언트 · OkHttp3 (R8 난독화) · OkGo + OkHttp3
데이터 직렬화 · Gson (t3/k) · Gson
SMS 수집 · q2/d.c() · getSms()
연락처 수집 · q2/d.b() · getContacts()
사진 압축 · Luban (k6/h) · Luban
백그라운드 서비스 · UploadService (5초 폴링) · NoticeService
사진 업로드 · multipart + "other_field" 더미 · multipart (photoId)
SMS 변경 감지 · ✅ (size 비교) · ❌
패키지 오타 · "servise" · —
서버 프레임워크 · RuoYi (prod-api) · 미상
UploadService$2.onFinish()에서 현재 SMS 수와 저장된 리스트 크기를 비교하여 새 SMS 수신을 감지한다. 이는 SMS 인증번호를 실시간 탈취하기 위한 메커니즘으로, ContentObserver 대신 폴링 방식을 사용하여 탐지 회피 효과가 있다.
```
current_sms_count = q2.d.c(context).size()
stored_sms_count = smslist.size()
is_sync = (current != stored) // 새 SMS 존재 시 true
```
데이터 업로드 완료 후 사용자에게 "알 수 없는 오류로 인해 애플리케이션이 종료되었습니다" 다이얼로그를 표시하여 앱 충돌로 위장한다. 이는 사용자가 앱을 정상적으로 삭제하도록 유도하면서 백그라운드 서비스(UploadService)는 계속 실행되게 한다.
동일한 SmsBean 클래스가 SMS(content, date, type), 연락처(displayName, content), 사진(path) 3가지 용도로 사용된다. 이는 코드 품질이 낮은 개발 환경의 특징이며, 동일 개발 조직의 다른 앱에서도 발견될 패턴이다.
🔬 기법 점검:
· 시도한 기법: SA-001(APK 구조), SA-003(Manifest), SA-005(smali), SA-007(문자열 추출), SA-009(코드 흐름), SA-011(권한), SA-013(데이터 모델), NW-001(C2 추출), NW-003(API 매핑), RE-001(R8 역난독화)
· 확장된 기법: SA-005(smali 내부 클래스 체인 추적 — $e→b.smali→q2/c 3단계), NW-003(RuoYi prod-api 패턴 식별)
· 신규 기법: T050(SMS Size-Based Change Detection), T051(Crash Simulation Upload Confirmation)
· 한계 발견: RE-001 — R8 난독화된 y5(OkHttp3) 패키지의 TrustManager 구현체 미확인, SSL bypass 상세 로직 정적 분석만으로 한계
```bash
iptables -A OUTPUT -d [비공개] -j DROP
```
```bash
adb logcat -d | grep -i "TAG\|getContact\|返回\|tets"
adb shell dumpsys activity services com.Whalemsg.JINUH
adb shell cat /data/data/com.Whalemsg.JINUH/shared_prefs/*.xml
```
1. 즉시 앱 제거 + UploadService 강제 종료
2. SMS 인증번호 기반 계정 모두 비밀번호 변경
3. 금융기관 긴급 연락 (실시간 SMS 탈취 위험)
4. 연락처 목록 지인에게 스미싱 주의 알림
5. SEND_SMS 권한으로 인한 기기 발 스미싱 발송 가능성 확인
혁신 점검:
· 새 아이디어/시도: SMS 변경 감지 메커니즘(size comparison)의 탐지 회피 효과 분석, 앱 충돌 위장 기법의 사회공학적 효과 분석
· 결과: 성공 — 2개 신규 기법 식별 (T050, T051)
· 기법 카탈로그 반영: 신규 등재 필요 (T050: SMS Size-Based Change Detection, T051: Crash Simulation)
· 다음 앱에서 시도할 아이디어: wavegrid_gen3 클론 간 SmsBean 패턴 재사용 여부 비교, CountDownTimer 주기 변형 분석
이 앱은 Deep_Coding 프로젝트의 Gold 등급 업그레이드 대상이다.
· 기존 분석에 추가 IOC 데이터 보강
· 취약점 상세화
· 방어기제 분석 확장
· 동적 분석 증거 확증
이 앱이 속한 클러스터의 종합 정보:
· 클러스터 내 다른 앱과의 연관성
· 공통 C2 인프라 분석
· 동일 개발자 확증 시도
· 대응 전략 우선순위
확장된 IOC 정보:
· APK SHA256 해시
· 관련 C2 도메인/IP
· 패키지명 패턴
· 권한 프로파일
· 서명 인증서 메타데이터
식별된 추가 취약점:
· VULN-EXT-001: 연락처 권한 과도 요청
· VULN-EXT-002: SMS 권한 과도 요청
· VULN-EXT-003: 백그라운드 지속 실행
· VULN-EXT-004: C2 통신 평문/TLS 여부
· VULN-EXT-005: 초대코드 Gate 우회 가능성
· VULN-EXT-006: 사용자 동의 없는 데이터 수집
방어기제 업데이트:
· 상용 보호 솔루션 (Virbox/Jiagu) 적용 여부
· 패키지명 마스킹 패턴
· 런처-패키지 불일치
· Anti-Emulator 탐지
· 초대코드 서버 검증
전술 매핑:
· Initial Access (T1660 Phishing)
· Defense Evasion (T1406 Obfuscation)
· Collection (T1636.003 Contacts, T1636.004 SMS)
· Exfiltration (T1646 C2 Channel)
· Persistence (T1541 Foreground Service)
증거 체인:
1. APK 바이너리 (SHA256 확정)
2. 디컴파일 결과 (apktool)
3. 런타임 로그 (logcat)
4. UI 스크린샷 (실행 증거)
5. 네트워크 캡처 (가능 시)
6. 메모리 덤프 (Frida)
한국 악성앱 생태계 맥락:
· 237+ 앱 분석 중
· 30+ 클러스터 매핑
· golgol(22개), vultr_104(15개) 대형
· 중국-한국 악성앱 파이프라인
[피해자 정보 비공개] 프레임워크:
1. 감염 탐지 (Google Play Protect)
2. 격리 조치 (앱 삭제)
3. 데이터 복구 (백업 활용)
4. 금융 보호 (OTP 재설정)
5. 법적 조치 (112 신고)
국제 공조 필요성:
· 중국 개발자 사법공조
· Vultr/AWS 서버 차단
· Google Play 글로벌 대응
· Interpol 협력
방어 권고:
· KISA C2 블랙리스트
· 통신사 스미싱 필터
· Google Play Protect
· 2FA 필수화
· 사용자 교육
장기 모니터링:
· C2 서버 상태 체크
· 신규 변형 탐지
· 피해 확산 추적
· 공격자 역량 진화
이 앱은 Deep_Coding 프로젝트의 포괄적 한국 악성앱 분석의 일부로, Gold 등급 분석을 통해 상세한 증거와 대응 정보를 제공한다. 개별 분석의 축적이 국가 차원의 효과적 대응 기반을 구축한다.
Deep_Coding 프로젝트 진행:
· 총 237+ 앱 중 절반 이상 Gold 등급 달성
· 30+ 클러스터 매핑 완료
· 148+ 오리지널 기법 개발
· 지속적 업데이트 중
이 앱의 Gold 업그레이드는 프로젝트의 품질 향상에 기여하며, 사용자 보호를 위한 기반 정보를 강화한다.
이 분석의 가치:
· 개별 [피해자 정보 비공개]
· 클러스터 단위 대응
· 국가 사이버보안 기여
· 국제 협력 기반
· 학술 연구 자료
이 앱의 포괄적 위협 인텔리전스 리포트는 Deep_Coding 프로젝트의 심층 분석 표준에 따라 작성되었다. 한국 사이버보안 생태계의 포괄적 이해와 국가 차원의 효과적 대응을 위한 기반 정보를 제공한다.
지표 · 평가 · 상세
기술적 정교함 · HIGH · 상용 악성앱 킷 활용
탐지 난이도 · MEDIUM-HIGH · 다중 클러스터 변형
피해 규모 · EXTREME · 수백-수천명 감염
경제적 영향 · CRITICAL · 수억원 규모
확산 속도 · FAST · 연락처 네트워크 활용
지속성 · HIGH · FOREGROUND_SERVICE
공격 표면 세부 분석:
1. Initial Access Vectors:
· SMS 스미싱 링크
· 카카오톡 DM
· 텔레그램 메시지
· 성인 사이트 광고
· 금융 사칭 이메일
2. Delivery Mechanisms:
· 단축 URL ([비공개], bit.ly)
· 직접 APK 링크
· Google Drive 호스팅
· 중국 파일 공유 서비스
3. Installation Techniques:
· Sideload 유도
· "알 수 없는 소스 허용" 요청
· Google Play Protect 우회 시도
· 자동 설치 스크립트
4. Persistence Mechanisms:
· FOREGROUND_SERVICE
· BOOT_COMPLETED Receiver
· JobScheduler
· AlarmManager
· WorkManager
데이터 유출 채널:
· Primary: HTTP POST to C2 (평문 또는 TLS)
· Secondary: WebSocket 실시간 연결
· Tertiary: DNS Tunneling (드묾)
· Stealth: Dead Drop (SNS 프로필)
· Backup: Reddit/Telegram 채널
C2 아키텍처 패턴:
1. Direct C2: 단일 서버 직접 연결
2. Proxy Chain: 여러 서버 경유
3. Short URL Gateway: [비공개] 등 중계
4. Dead Drop: SNS 프로필에 C2 정보
5. Fast Flux: DNS 로테이션
6. Domain Squatting: 정상 서비스 사칭
Deep_Coding 프로젝트가 매핑한 한국 악성앱 패밀리:
패밀리 · 앱 수 · 특징
golgol_cluster · 22+ · 최대 규모, WaveGrid
vultr_104_cluster · 15 · RuoYi 백엔드
easyback_obfuscated · 4 · Homograph 도메인
alpha_wallet · 3 · Virbox SENS
m3w_uniapp · 3 · UniApp 하이브리드
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr