https://mobilehacking.kr/challenges/
write up 태그가 있어 풀이에 대해 공개 포스팅한다.
기능 분석 없이 바로 문제 풀이 시작하겠다.
익스플로잇
탐지 우회
처음 앱을 실행 시키면 Root 탐지와 Failed to load app components로 인해 앱이 제대로 실행되지 않는 것을 확인할 수 있다. 총 3개의 함수를 frida로 우회해야하는데, 그 함수는 아래와 같다.
private final void loadDynamicDex() {
try {
Log.d(TAG, "Starting Dynamic DEX Loading...");
File q2 = s0.a.q(this);
if (q2 == null) {
Log.e(TAG, "Failed to decrypt DEX");
Toast.makeText(this, "Failed to load app components", 1).show();
finish();
return;
}
Log.d(TAG, "DEX decrypted successfully: " + q2.getAbsolutePath());
DexClassLoader dexClassLoader = null;
try {
m.j = new DexClassLoader(q2.getAbsolutePath(), getDir("odex", 0).getAbsolutePath(), null, getClassLoader());
Log.d("DexClassLoader", "DEX loaded successfully");
dexClassLoader = m.j;
} catch (Exception e2) {
Log.e("DexClassLoader", "Failed to load DEX", e2);
}
if (dexClassLoader == null) {
Log.e(TAG, "Failed to load DEX");
Toast.makeText(this, "Failed to load app components", 1).show();
finish();
return;
}
Log.d(TAG, "DEX loaded successfully");
try {
File file = new File(getDir("dex", 0), "core.dex");
if (file.exists()) {
file.delete();
Log.d("DexDecryptor", "Cleaned up decrypted DEX file");
}
} catch (Exception e3) {
Log.e("DexDecryptor", "Failed to cleanup DEX", e3);
}
} catch (Exception e4) {
Log.e(TAG, "Error during Dynamic DEX Loading", e4);
Toast.makeText(this, "App initialization failed", 1).show();
finish();
}
}
private final void performInitialSecurityCheck() {
Context applicationContext = getApplicationContext();
c.m(applicationContext, "getApplicationContext(...)");
b D = c.D(applicationContext);
if (D.f1363d) {
return;
}
Toast.makeText(getApplicationContext(), D.f1362c ? "Security Alert: Frida detected\nApp cannot run" : D.f1361b ? "Security Alert: Debugger detected\nApp cannot run" : D.f1360a ? "Security Alert: Rooted device detected\nApp cannot run" : "Security Alert: Security check failed\nApp cannot run", 1).show();
finishAndRemoveTask();
Process.killProcess(Process.myPid());
System.exit(1);
}
private final void startSecurityMonitoring() {
s0.a.F(c.b(i0.f567b), null, 0, new f(this, null), 3);
}
loadDynamicDex(), performInitialSecurityCheck(), startSecurityMonitoring()을 frida로 우회하는 코드는 아래와 같다.
Java.perform(function() {
console.log("[+] Starting bypass script");
var MainActivity = Java.use("mobilehacking.kr.smartgrid.MainActivity");
MainActivity.performInitialSecurityCheck.implementation = function() {
console.log(" [!!] performInitialSecurityCheck() execution BLOCKED/SKIPPED.");
return;
};
MainActivity.loadDynamicDex.implementation = function() {
console.log(" [!!] loadDynamicDex() execution BLOCKED/SKIPPED.");
return;
}
MainActivity.startSecurityMonitoring.implementation = function() {
console.log(" [!!] startSecurityMonitoring() execution BLOCKED/SKIPPED.");
return;
}
console.log("[+] Bypass applied. Resuming application...");
});
위 코드를 해석해보면 각 함수가 실행될 때, 본래 수행하는 코드를 전부 건너뛰고 return하도록 한다. frida 실행 명령어는 아래와 같다.
frida -U -f mobilehacking.kr.smartgrid -l jwcs.js
admin 계정 탈취

탐지 우회를 하고 나면 로그인 창이 뜬다. 기본 계정으로 demo가 주어지지만 flag에 대한 내용은 얻을 수 없었다.

/data/data/mobilehacking.kr.smartgrid/databases에서 데이터베이스 파일을 획득할 수 있었다.

여기서 admin에 대한 계정 정보를 확인할 수 있었다. demo 계정은 crackstation을 통해서 비밀번호를 크랙할 수 있었지만, admin은 안됐다. 그래서 코드를 좀 더 확인해봤다.

demo 계정과 admin 계정의 비밀번호 비교 방식이 다른 것을 확인할 수 있다. 여기서 q1.a()를 확인해본 결과 아래와 같다.
package q1;
import androidx.activity.w;
import d1.a0;
import j0.r;
import java.nio.charset.Charset;
import s0.c;
/* loaded from: classes.dex */
public final class a {
/* renamed from: a, reason: collision with root package name */
public final int[] f1359a = new int[256];
public final String a(String str) {
c.n(str, "data");
Charset charset = c1.a.f471a;
byte[] bytes = str.getBytes(charset);
c.m(bytes, "this as java.lang.String).getBytes(charset)");
byte[] bytes2 = a0.t(a0.f523p).getBytes(charset);
c.m(bytes2, "this as java.lang.String).getBytes(charset)");
b(bytes2);
return r.x0(c(bytes), w.f165o);
}
public final void b(byte[] bArr) {
int[] iArr;
int i = 0;
while (true) {
iArr = this.f1359a;
if (i >= 256) {
break;
}
iArr[i] = i;
i++;
}
int i2 = 0;
for (int i3 = 0; i3 < 256; i3++) {
int i4 = iArr[i3];
i2 = ((i3 * 3) + ((i2 + i4) + bArr[i3 % bArr.length])) % 256;
iArr[i3] = iArr[i2];
iArr[i2] = i4;
}
}
public final byte[] c(byte[] bArr) {
byte[] bArr2 = new byte[bArr.length];
int length = bArr.length;
int i = 0;
int i2 = 0;
for (int i3 = 0; i3 < length; i3++) {
i = (i + 1) % 256;
int[] iArr = this.f1359a;
int i4 = iArr[i];
i2 = (i2 + i4) % 256;
iArr[i] = iArr[i2];
iArr[i2] = i4;
bArr2[i3] = (byte) (iArr[(iArr[i] + i4) % 256] ^ bArr[i3]);
}
return bArr2;
}
}
암복호화 관련 코드이고, 여기서 필요한 값은
- a0.t(a0.f523p)
- db에 저장된 admin password 값
총 2개이다.

`/data/data/mobilehacking.kr.smartgrid/databases`에서 데이터베이스 파일을 확인할 수 있으며, 그 결과는 아래와 같다.

키 값도 코드를 따라가다보면 구할 수 있다. 그렇게 복호화 코드는 아래와 같다.
import codecs
# --- 1. 키 생성 로직 (d1.a0.t(d1.a0.f523p) 구현) ---
def generate_rc4_key():
"""Java 코드 d1.a0.t 함수를 사용하여 실제 RC4 키를 추출합니다."""
# d1.a0.f523p 값
f523p_encrypted = [
61, 40, 105, 105, 52, 10, 106, 45, 105, 40, 9, 105, 57, 47, 40, 107, 46, 35
]
# 복호화 키 (90)
DECRYPT_KEY = 90
decrypted_bytes = bytearray()
for encrypted_byte in f523p_encrypted:
# XOR 연산: b2 ^ 90
decrypted_byte = encrypted_byte ^ DECRYPT_KEY
decrypted_bytes.append(decrypted_byte)
# 문자열로 변환하여 반환
return decrypted_bytes.decode('utf-8')
# --- 2. 변형된 RC4 복호화기 (q1.a 클래스 구현) ---
class ModifiedRC4Decryptor:
def __init__(self, key_bytes):
"""키 스케줄링 알고리즘 (KSA) - q1.a.b() 함수"""
self.S = list(range(256))
j = 0
key_len = len(key_bytes)
for i in range(256): # 자바 코드의 i3
val_i = self.S[i] # 자바 코드의 i4
# [중요] 변형된 KSA 로직 반영: i2 = ((i3 * 3) + ((i2 + i4) + bArr[i3 % bArr.length])) % 256;
j = ((i * 3) + (j + val_i + key_bytes[i % key_len])) % 256
# Swap
self.S[i], self.S[j] = self.S[j], self.S[i]
def decrypt(self, data_bytes):
"""의사 난수 생성 알고리즘 (PRGA) - q1.a.c() 함수"""
S = self.S[:]
i = 0
j = 0
res = bytearray()
for byte in data_bytes:
i = (i + 1) % 256
val_i = S[i] # i4
j = (j + val_i) % 256 # i2
# Swap
S[i] = S[j]
S[j] = val_i
# 키 스트림 생성 및 XOR (iArr[(iArr[i] + i4) % 256] ^ bArr[i3])
k = S[(S[i] + val_i) % 256]
res.append(byte ^ k)
return bytes(res)
# ==================================================
# 실행
# ==================================================
# 1. 암호화된 입력 데이터 (16진수 문자열)
INPUT_HEX_STRING = "fd48d4edbe78b5fda5214c"
print(f"암호화된 16진수 데이터: {INPUT_HEX_STRING}")
print("---")
# 2. 키 추출
key_string = generate_rc4_key()
key_bytes = key_string.encode('utf-8')
print(f"RC4 최종 키 (문자열): {key_string}")
# 3. 데이터 준비
try:
encrypted_data = bytes.fromhex(INPUT_HEX_STRING)
except ValueError:
print("오류: 입력된 문자열은 올바른 16진수 형식이 아닙니다.")
exit()
# 4. 복호화 실행
decryptor = ModifiedRC4Decryptor(key_bytes)
decrypted_data = decryptor.decrypt(encrypted_data)
# 5. 결과 출력
print("---")
print(f"복호화된 데이터 (Hex): {decrypted_data.hex()}")
try:
final_result = decrypted_data.decode('utf-8')
print(f"복호화 결과 (Text): {final_result}")
except UnicodeDecodeError:
print("복호화된 데이터는 텍스트가 아닌 바이너리 데이터입니다.")

짜잔
복호화된 결과를 입력하고 admin으로 로그인하면 flag에 대해 알 수 있다.
'안드로이드 해킹 > mobilehacking.kr' 카테고리의 다른 글
| [mobilehacking.kr] Private Block App 풀이 (0) | 2025.07.27 |
|---|---|
| [mobilehacking.kr] android_issue_2020 Deeplink 풀이 (0) | 2025.07.27 |
| [mobilehacking.kr] android_issue_2020 Root Detection bypass 풀이 (0) | 2025.07.27 |
| [mobilehacking.kr] android_issue_2020 User Enumeration 풀이 (0) | 2025.07.26 |
| [mobilehacking.kr] SolidApp 풀이 (0) | 2025.07.24 |