분석일: 2026-02-13 (심층 분석)
초기 분석일: 2026-02-12
분석자: 주식회사 아크링크 ([비공개] .ai, deep-scan.ai)
분석 목적: 피싱/스미싱 [피해자 정보 비공개] 위한 악성앱 분석
보고서 버전: v4.0 (ML Gold Standard Extended)
분석 Phase: 22
항목 · 내용
**파일명** · Y.apk
**표시 이름** · Y
**외부 패키지** · com.dsjdsjkds.cndjjytqvxd
**내부 패키지** · com.xyx.sms
**앱 패밀리** · Wavegrid Gen3 (SMS) - 이전 API 버전
**Compile SDK** · 32 (Android 12)
**프레임워크** · Java + OkHttp + Huawei ScanKit [비공개] + ML Kit [비공개]
**DEX 구조** · 단일 DEX (smali/ 1개)
**총 smali 파일** · 4,243개
**C2 클러스터** · [비공개]
**위협 수준** · **High** (23/30)
컴포넌트 · 클래스명 · 역할
**Application** · com.xyx.sms.BaseApp · 생명주기 감시 (불완전 구현)
**Launcher** · com.xyx.sms.LoginActivity · 초대코드 입력 + QR 스캔
**Main** · com.xyx.sms.MainActivity · 메인 화면
**Contact** · com.xyx.sms.ContactActivity · 연락처 표시
**Album** · com.xyx.sms.AlbumActivity · 앨범 표시
**Upload** · com.xyx.sms.UploadActivity · 데이터 수집/업로드 허브
**Service** · com.xyx.sms.servise.UploadService · 백그라운드 SMS 수집
> 주의: servise (오타) - ze/zine/Flirting_95/138과 동일한 오타 유지
특성 · ze · zine · Flirting_95 · Flirting_138 · **Y**
**SDK** · 34 · 34 · 34 · 34 · **32**
**DEX** · 단일 · 단일 · 2-DEX · 2-DEX · **단일**
**smali** · 4,352 · 4,352 · 6,679 · 7,484 · **4,243**
**난독화** · h4/ (선택적) · h4/ (선택적) · 없음 · 부분 · **g4/f4/ (선택적)**
**초대코드** · if-ne (정상) · if-eq (반전) · if-eq (반전) · if-eq (반전) · **if-eq (반전)**
**BaseApp** · 없음 · 완전 · 완전 · 완전 · **불완전**
**SMS 필터** · 1일 · 1일 · 코드만(미적용) · 전체 · **코드만(미적용→전체)**
**API 경로** · /AppUser/ · /AppUser/ · /AppUser/ · /AppUser/ · **/webapp/ (직접)**
**C2 클러스터** · xxayjgagagxxg · xxayjgagagxxg · pinonaoshsfpaohjgadsf · xxayjgagagxxg · **pinonaoshsfpaohjgadsf**
> 핵심 발견: Y.apk만 유일하게 /AppUser/ 계층 없는 API 경로 사용
> SDK 32 + 불완전 BaseApp + 구 API → pinonaoshsfpaohjgadsf 클러스터의 초기 변종
# · 권한 · 위험도 · 목적
1 · `READ_CONTACTS` · **Critical** · 연락처 전체 탈취
2 · `READ_PHONE_STATE` · **High** · IMEI, 전화번호 수집
3 · `READ_PHONE_NUMBERS` · **High** · 전화번호 직접 읽기
4 · `READ_EXTERNAL_STORAGE` · **High** · 저장소 파일 접근
5 · `READ_MEDIA_IMAGES` · **High** · 사진 탈취 (Android 13+)
6 · `READ_MEDIA_AUDIO` · **High** · 음성 파일 탈취
7 · `READ_MEDIA_VIDEO` · **High** · 동영상 탈취
8 · `INTERNET` · Normal · 네트워크 통신
9 · `ACCESS_NETWORK_STATE` · Normal · 네트워크 상태 확인
권한 · Manifest · 런타임 · 위치
`READ_SMS` · ❌ 미선언 · ✅ 5곳에서 요청 · LoginActivity:247, UploadActivity:232/703, UploadActivity$b:55, f4/a:69
> 기법 T022: READ_SMS를 Manifest에 선언하지 않고 런타임에만 요청하여 정적 분석 회피
> - ze/zine/Flirting_95/Flirting_138과 동일한 기법
> - 단, Flirting_138만 Manifest에 READ_SMS 명시 (진화 과정에서 추가)
Y.apk는 g4/ + f4/ 패턴 사용 (ze/zine의 h4/와 다름):
난독화 · 원본 (Flirting_95 역검증) · 용도
`g4/a` · GlideEngine · 이미지 로딩 (Glide 라이브러리)
`g4/b` · HttpUtils (variant) · HTTP POST (JSON)
`g4/c` · HttpUtils (main) · HTTP POST (JSON) - C2 URL 포함
`g4/d` · ReadContactUtils · 연락처/SMS/미디어 수집
`g4/e` · SPUtils · SharedPreferences 관리
`f4/a` · (CountDownTimer) · UploadService 5초 타이머
`f4/b` · (SMS Sender Thread) · SMS 전송 스레드 (saveSms)
> 비교: ze/zine는 h4/c=HttpUtils, h4/d=ReadContactUtils, h4/e=SPUtils
> 문자가 다르지만(g4 vs h4) 구조와 매핑은 동일 → 같은 소스의 다른 ProGuard 빌드
```
com/xyx/sms/ # 핵심 악성 코드 (33개 파일)
├── a.smali # 유틸리티
├── b.smali # 이미지 업로드 + C2 URL
├── BaseApp.smali # Application (생명주기 감시)
├── BaseApp$a.smali # ActivityLifecycleCallbacks (불완전)
├── bean/ # 데이터 모델
├── LoginActivity*.smali # 초대코드 (5개 파일)
├── MainActivity*.smali # 메인 화면 (4개 파일)
├── UploadActivity*.smali # 데이터 수집 (9개 파일)
├── ContactActivity*.smali # 연락처 (2개 파일)
├── AlbumActivity*.smali # 앨범 (4개 파일)
└── servise/ # 서비스 (오타 유지)
└── UploadService.smali
g4/ # 난독화 유틸리티 (7개 파일)
├── a.smali, a$a.smali # GlideEngine
├── b.smali # HttpUtils (variant)
├── c.smali # HttpUtils (main, C2 URL)
├── d.smali # ReadContactUtils
└── e.smali, e$a.smali # SPUtils
f4/ # 난독화 서비스 헬퍼 (3개 파일)
├── a.smali # CountDownTimer (5초 무한루프)
└── b.smali, b$a.smali # SMS 전송 스레드
```
항목 · 내용
**도메인** · [비공개]
**Base URL** · `[비공개]
**프로토콜** · HTTP (평문) - MITM 완전 노출
**프레임워크** · RuoYi (Spring Boot)
**C2 공유** · Flirting_95 (동일 도메인), CherryTalk (.buzz 변종)
파일 · 라인 · URL
`com/xyx/sms/b.smali` · 404 · `[비공개]
`g4/c.smali` · 135 · `[비공개]
# · 메소드 · 엔드포인트 · 용도 · 위치
1 · POST · `[비공개] · 초대코드 검증 (반전 로직) · LoginActivity$a$a.smali:66
2 · POST · `[비공개] · 연락처 일괄 업로드 · UploadActivity$f.smali:66
3 · POST · `[비공개] · SMS 업로드 (1일 필터) · UploadActivity$g.smali:66, f4/b.smali:49
4 · POST · `[비공개] · 이미지/비디오 업로드 · b.smali:152
Y.apk vs 다른 Gen3 변종 API 비교:
기능 · Y (구 API) · ze/zine/Flirting_95/138 (신 API)
초대코드 · `webapp/login` · `webapp/AppUser/checkInviteCode`
연락처 · `webapp/saveAddressBook` · `webapp/AppUser/upPhoneBook`
SMS · `webapp/saveSms` · `webapp/AppUser/saveSms`
이미지 · `webapp/image/{userName}` · `webapp/AppUser/upPic`
분석:
· Y는 /AppUser/ 계층 없이 직접 /webapp/ 하위에 엔드포인트 배치
· 엔드포인트 이름도 다름: login vs checkInviteCode, saveAddressBook vs upPhoneBook
· 동일 RuoYi 프레임워크 + 동일 기능이지만 API 버전이 다름
· 이는 Y가 서버 API 리팩토링 이전 버전을 사용하고 있음을 의미
· 결론: Y는 pinonaoshsfpaohjgadsf 클러스터의 초기 빌드
파일: res/xml/network_security_config.xml
```xml
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
```
분석:
· cleartextTrafficPermitted="true" → 모든 도메인에 대해 HTTP 평문 통신 허용
· Android 9 (API 28)부터 기본적으로 cleartext 차단인데, 이를 명시적으로 재허용
· AndroidManifest.xml에서 android:networkSecurityConfig="@xml/network_security_config" 참조
보안 영향:
위험 · 설명 · 심각도
MITM 노출 · 모든 C2 통신이 평문 HTTP · **Critical**
데이터 가로채기 · 연락처/SMS/이미지가 암호화 없이 전송 · **Critical**
트래픽 분석 · 네트워크 모니터링으로 즉시 C2 URL 식별 가능 · High
패킷 변조 · 중간자가 서버 응답을 변조하여 앱 동작 조작 가능 · High
> Gen3 패밀리 비교: ze/zine/Flirting_95/Flirting_138 모두 동일한 cleartext 허용 설정 사용.
> 이는 개발자가 HTTPS 인프라를 구축하지 않았거나, SSL 인증서 비용을 절감한 것으로 추정.
Y.apk는 2개의 독립적 HTTP 클라이언트를 사용합니다.
소스: HttpUtils.java (ProGuard 후 g4/c)
```smali
const-string v2, "application/json; charset=utf-8"
invoke-static {v2}, Lp4/t;->a(Ljava/lang/String;)Lp4/t;
new-instance v3, Lcom/google/gson/j; # GsonBuilder
invoke-virtual {v3}, Lcom/google/gson/j;->a()Lcom/google/gson/i; # build()
invoke-virtual {v3, p1}, Lcom/google/gson/i;->e(Ljava/lang/Object;)Ljava/lang/String; # toJson()
const-string v5, "[비공개]
const-string p0, "POST"
invoke-virtual {p1, p0, v2}, Lp4/x$a;->b(Ljava/lang/String;Lp4/a0;)V
iget p1, p0, Lp4/b0;->c:I # 응답 코드
const/16 v1, 0xc8 # 200
if-lt p1, v1, :cond_0 # < 200이면 실패
const/16 v1, 0x12c # 300
if-ge p1, v1, :cond_0 # >= 300이면 실패
```
사용처: 초대코드 검증(webapp/login), 연락처 업로드(webapp/saveAddressBook), SMS 업로드(webapp/saveSms)
직렬화 체인: HashMap<String,String> → Gson.toJson() → RequestBody.create() → OkHttp POST
소스: UploadActivity.java 내부 클래스 (이미지 전용)
```smali
invoke-static {}, Ljava/util/UUID;->randomUUID()Ljava/util/UUID;
invoke-virtual {v4}, Ljava/util/UUID;->toString()Ljava/lang/String;
const-string v8, "multipart"
invoke-virtual {v7, v8}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
const-string v7, "file" # 파트 이름: "file"
invoke-virtual {v1}, Ljava/io/File;->getName()Ljava/lang/String; # 파일명
const-string v9, "image/*" # MIME 타입
invoke-static {v9}, Lp4/t;->a(Ljava/lang/String;)Lp4/t; # MediaType
const-string v1, "other_field" # 파트 이름: "other_field"
const-string v7, "value" # 값: "value"
```
분석:
· file 파트: 실제 이미지 파일 (image/* MIME 타입)
· other_field = "value" → 의미 없는 더미 필드 (개발자가 샘플 코드를 복사한 흔적)
· UUID boundary로 multipart 구분 → 표준 OkHttp Multipart 패턴
· b.smali:152에서 URL 경로 webapp/image/{userName} 조합
> 핵심 발견: other_field = "value" 더미 필드는 OkHttp 멀티파트 튜토리얼 코드의 원본 그대로 사용.
> 개발자가 스택오버플로우/공식 문서에서 코드를 복사하면서 더미 필드를 제거하지 않음.
서버 응답 파싱 (g4/c.smali:249-276):
```smali
const-string v1, "\u8fd4\u56de="
invoke-virtual {p1, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-static {p0}, Lcom/alibaba/fastjson/JSON;->parseObject(Ljava/lang/String;)Lcom/alibaba/fastjson/JSONObject;
const-string v0, "code"
invoke-virtual {p1, v0}, Lcom/alibaba/fastjson/JSONObject;->getInteger(Ljava/lang/String;)Ljava/lang/Integer;
```
직렬화 라이브러리 이중 사용:
방향 · 라이브러리 · 사용처
요청 직렬화 · **Gson** · g4/c.smali (HashMap → JSON)
응답 파싱 · **FastJSON** (Alibaba) · g4/c.smali (JSON → JSONObject)
데이터 직렬화 · **FastJSON** · UploadActivity (ArrayList → JSON)
> 설계 결함: 동일 앱에서 Gson과 FastJSON을 혼용. 요청은 Gson으로 직렬화하고 응답은 FastJSON으로 파싱.
> 이는 서로 다른 코드 소스(튜토리얼)에서 복사한 것을 합치면서 라이브러리 통일을 하지 않은 증거.
파일: LoginActivity$a$a$a$a.smali
핵심 코드 (line 80-84):
```smali
const/16 v2, 0xc8 # v2 = 200
if-eq v0, v2, :cond_1 # 200 == 응답코드이면 → :cond_1 (에러 분기)
```
분기 흐름:
· if-eq 조건 참 (응답 200): :cond_1 → JSON "msg" 파싱 → 에러 Toast
· if-eq 조건 거짓 (응답 ≠ 200): fall-through → UUID 저장(line 132) → invite_code 저장(line 201) → "success" Toast(line 275) → UploadActivity 시작
우회: 임의의 초대코드 입력 → 서버가 200 반환 → 앱이 에러로 처리 → 비정상 응답 시 성공으로 처리
> 진화 계보: ze(if-ne, 정상) → zine(if-eq, 최초 반전) → Y/Flirting_95/Flirting_138(if-eq, 계승)
BlankJ UtilCode 기반:
· Build.FINGERPRINT, Build.MODEL, Build.MANUFACTURER 검사
· ze/zine/Flirting_95/Flirting_138과 동일 패턴
· 우회: Build 속성 const-string 패치
파일: BaseApp$a.smali (line 110-135)
```smali
.method public final onActivityStopped(Landroid/app/Activity;)V
new-instance p1, Landroid/content/Intent;
const-class v1, Lcom/xyx/sms/servise/UploadService;
invoke-direct {p1, v0, v1}, Landroid/content/Intent;-><init>(...)V
return-void # ← startService() 호출 없음!
.end method
```
분석:
· Intent를 생성하지만 startService()를 호출하지 않음
· zine/Flirting_95/Flirting_138에서는 startService() 완성
· 이것은 BaseApp 메커니즘의 초기 미완성 구현
· 코드 작성자가 Intent 패턴을 준비했지만 실행 코드를 빠뜨림
> 진화적 의미: ze(없음) → Y(불완전) → zine(완성) → Flirting_95/138(계승)
START_STICKY (UploadService.smali:155):
```smali
const/4 p1, 0x1 # START_STICKY = 1
return p1
```
CountDownTimer 무한루프 (f4/a.smali):
```smali
const-wide/16 v0, 0x1388 # 5000ms
const-wide/16 v2, 0x3e8 # 1000ms tick
```
· onFinish() → 새 CountDownTimer 생성 → start() → 무한 반복
· 5초마다 SMS 재수집 시도
```
[사용자 입력] → LoginActivity$a$a → HTTP POST webapp/login
↓
[서버 응답] → LoginActivity$a$a$a → JSON 파싱
↓
[코드 비교] → LoginActivity$a$a$a$a
├── code == 200 (정상) → :cond_1 → "msg" → 에러 Toast ❌
└── code ≠ 200 (비정상) → fall-through → UUID 저장 → 성공 ✅
```
1. 서버에 유효한 초대코드 전송 → 서버 응답 200 → 앱이 에러 처리
2. 서버에 무효한 초대코드 전송 → 서버 응답 ≠ 200 → 앱이 성공 처리
3. 서버 접속 불가 → 네트워크 에러 → 앱이 성공 처리
결과: 어떤 초대코드든, 심지어 서버가 꺼져있어도 우회 가능
파일: g4/d.smali method b (line 171-430)
· ContentResolver.query() → ContactsContract.CommonDataKinds.Phone
· 수집 필드: display_name, data1 (전화번호), contact_id
· 필터: 없음 (전체 수집)
· 전송: webapp/saveAddressBook (JSON 배열)
· 주의: g4/d.smali의 b() 메서드는 직접 호출처가 발견되지 않음 (UploadActivity에서 별도 경로 사용 가능)
파일: g4/d.smali method c (line 432-689)
```smali
Calendar.getInstance()
Calendar.add(5, -1) # DAY_OF_MONTH, -1일
Calendar.getTimeInMillis() # 타임스탬프 생성 → 변수에 저장
invoke-virtual/range {v2 .. v7}
Uri.parse("content://sms/")
```
핵심 발견: Calendar 계산은 수행하지만 ContentResolver.query()의 selection 파라미터가 null.
Flirting_95와 동일한 패턴 → pinonaoshsfpaohjgadsf 클러스터 전체가 SMS 필터 미적용.
ze/zine(xxayjgagagxxg 클러스터)만 필터가 실제 적용됨.
전송 페이로드 (f4/a.smali 분석):
```json
{
> [비공개]
"bjPhone": "<피해자 전화번호>",
"content": [
{"address": "발신번호", "body": "내용", "date": "타임스탬프", "type": "수신/발신"}
]
}
```
파일: com/xyx/sms/b.smali (line 152)
· webapp/image/{userName} 엔드포인트
· Multipart 업로드
· MediaStore에서 수집한 파일 전송
· TelephonyManager.getLine1Number() → bjPhone 필드
· Build.MODEL, Build.MANUFACTURER → 기기 식별
Y.apk에서 SmsBean 클래스는 SMS뿐 아니라 연락처와 이미지 경로에도 재활용됩니다.
SmsBean 필드 정의 (bean/SmsBean.smali):
```smali
.field public content:Ljava/lang/String;
.field public date:Ljava/lang/String;
.field public displayName:Ljava/lang/String;
.field public path:Ljava/lang/String;
.field public type:Ljava/lang/String;
```
3가지 용도로 재활용되는 패턴:
용도 · 메서드 · 사용 필드 · 미사용 필드
**SMS** · g4/d.c() · content, displayName(=address), date, type · path
**연락처** · g4/d.b() · displayName(=이름), content(=전화번호) · date, type, path
**이미지** · g4/d.a() · path(=파일경로) · content, displayName, date, type
g4/d.smali 바이트코드 증거:
```smali
iput-object v4, v5, Lcom/xyx/sms/bean/SmsBean;->content:Ljava/lang/String; # body
iput-object v1, v5, Lcom/xyx/sms/bean/SmsBean;->displayName:Ljava/lang/String; # address
iput-object v2, v5, Lcom/xyx/sms/bean/SmsBean;->date:Ljava/lang/String; # date
iput-object v3, v5, Lcom/xyx/sms/bean/SmsBean;->type:Ljava/lang/String; # type
iput-object v3, v5, Lcom/xyx/sms/bean/SmsBean;->displayName:Ljava/lang/String; # display_name
iput-object v4, v5, Lcom/xyx/sms/bean/SmsBean;->content:Ljava/lang/String; # data1 (전화번호!)
iput-object v1, v2, Lcom/xyx/sms/bean/SmsBean;->path:Ljava/lang/String; # _data (파일경로)
```
미사용 ContactBean:
```smali
.field public name:Ljava/lang/String;
.field public number:Ljava/lang/String;
```
> 분석: 연락처 전용 ContactBean이 존재하지만 실제 코드에서는 SmsBean으로 연락처를 저장.
> 이는 개발자가 ContactBean을 만든 후 실제 구현에서 SmsBean을 재활용하는 편의적 설계를 선택한 것.
> 원래 com.xyx.sms 패키지가 SMS 수집용으로 시작되어 SmsBean이 "범용 데이터 컨테이너"로 진화한 결과.
```
[앱 시작]
│
▼
LoginActivity.onCreate()
├── is_first 확인 (g4/e SPUtils, "share_data" SharedPreferences)
│ ├── true → UploadActivity.onCreate() (첫 방문이 아님)
│ └── false → 초대코드 UI 표시
│ ├── 입력 → LoginActivity$a.onClick()
│ │ ├── UUID 생성 (없으면)
│ │ ├── HashMap {name: 초대코드, cId: UUID}
│ │ ├── Thread(LoginActivity$a$a) → g4/c.a("webapp/login", map)
│ │ └── OnSuccess 콜백 → LoginActivity$a$a$a$a
│ │ ├── code == 200 → 에러 Toast (반전!)
│ │ └── code ≠ 200 → UUID/invite_code 저장 → UploadActivity
│ └── QR 스캔 (ScanKit - 미구현?)
│
▼
UploadActivity.onCreate()
├── invite_code 읽기 (g4/e SPUtils)
├── is_upload 확인 (g4/e SPUtils)
│ ├── "0" → 데이터 수집 실행
│ └── "1" → (이미 업로드됨, 추가 동작 불명)
├── TelephonyManager.getLine1Number() → 전화번호 (z 필드)
│
├── [버튼 $a] 전체 업로드
│ ├── s() → MediaStore 이미지 수집 → b.smali → POST webapp/image/{userName}
│ ├── t() → 연락처 수집 → HashMap → Thread → POST webapp/saveAddressBook
│ └── u() → SMS 수집 → HashMap → Thread → POST webapp/saveSms
│
├── [버튼 $b] SMS만
├── [버튼 $c] 연락처만
└── [버튼 $d] 앨범만
│
▼
UploadService (백그라운드)
├── START_STICKY (시스템 종료 후 재시작)
├── TelephonyManager.getLine1Number() → 전화번호
└── f4/a CountDownTimer(5000ms)
└── onFinish() 매 5초:
├── READ_SMS 권한 확인
├── SMS 수집 (g4/d.c())
├── 이전 수집과 크기 비교 (size != previous size)
│ ├── 변경 있음 → 새 Thread → f4/b → POST webapp/saveSms
│ └── 변경 없음 → skip
└── 새 CountDownTimer 생성 → start() (무한 루프)
```
파일: g4/e.smali (원본: SPUtils.java)
저장소 이름: "share_data" (Private Mode)
```smali
const-string v0, "share_data"
const/4 v1, 0x0 # MODE_PRIVATE
invoke-virtual {p0, v0, v1}, Landroid/content/Context;->getSharedPreferences(...)
```
상태 키 목록 (앱 전체에서 사용):
키 · 타입 · 기본값 · 설정 위치 · 읽기 위치 · 용도
`is_first` · Boolean · false · LoginActivity$a$a$a$a · LoginActivity.onCreate · 첫 방문 플래그
`UUID` · String · "" · LoginActivity$a$a$a$a · LoginActivity.onCreate · 기기 고유 ID
`invite_code` · String · "" · LoginActivity$a$a$a$a · UploadActivity, f4/a · 초대코드 (=userName)
`is_upload` · String · "0" · UploadActivity · UploadActivity.onCreate · 업로드 완료 플래그
SPUtils 타입 디스패치 (g4/e.smali:34-222):
```smali
instance-of v0, p2, Ljava/lang/String; → getString/putString
instance-of v0, p2, Ljava/lang/Integer; → getInt/putInt
instance-of v0, p2, Ljava/lang/Bool
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr