이번에는 게임 컨셉의 문제다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<activity
android:theme="@style/Theme.MarioWorld"
android:label="@string/app_name"
android:name="mobilehacking.kr.marioworld.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:theme="@style/Theme.MarioWorld"
android:label="@string/app_name"
android:name="mobilehacking.kr.marioworld.BossRoundActivity"
android:exported="true"
android:screenOrientation="sensorLandscape"
android:configChanges="screenSize|orientation|keyboardHidden"/>
</activity>
우선 있는 Activity는 Main과 BossRound이다.
게임을 켜보면 stage1이 있다.
굼바 비슷한 것이 있는 마리오 게임..? 조작 방법은 간단하고 stage 1을 깨면 stage 2가 생긴다.
스테이지 2를 깨면 이렇게 깬 시간과 함께 무슨 시드를 준다.
아직 잘 모르겠어서 exported=”true”인 BossRoundActivity를 열어본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final boolean booleanExtra = getIntent().getBooleanExtra("is_dev", false);
ComponentActivityKt.setContent$default(bossRoundActivity, null, ComposableLambdaKt.composableLambdaInstance(2131004178, true, new Function2<Composer, Integer, Unit>() { // from class: mobilehacking.kr.marioworld.BossRoundActivity.onCreate.1
@Override // kotlin.jvm.functions.Function2
public /* bridge */ /* synthetic */ Unit invoke(Composer composer, Integer num) {
invoke(composer, num.intValue());
return Unit.INSTANCE;
}
public final void invoke(Composer composer, int i) {
ComposerKt.sourceInformation(composer, "C:BossRoundActivity.kt#oaxw8q");
if ((i & 3) != 2 || !composer.getSkipping()) {
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventStart(2131004178, i, -1, "mobilehacking.kr.marioworld.BossRoundActivity.onCreate.<anonymous> (BossRoundActivity.kt:56)");
}
if (!booleanExtra) {
composer.startReplaceGroup(1681920500);
ComposerKt.sourceInformation(composer, "57@2460L14");
BossRoundActivityKt.DeniedScreen(composer, 0);
composer.endReplaceGroup();
} else {
composer.startReplaceGroup(1681973851);
ComposerKt.sourceInformation(composer, "59@2512L71");
ThemeKt.MarioWorldTheme(false, ComposableSingletons$BossRoundActivityKt.INSTANCE.m8180getLambda1$app_release(), composer, 48, 1);
composer.endReplaceGroup();
}
is_dev = true면 열리는 것 같다.
1
adb shell am start -n mobilehacking.kr.marioworld/.BossRoundActivity --ez is_dev true
보스전이 시작되고 10번의 공격을 피하면 깰 수 있다.
그랬더니 무슨 phrase를 입력하라고 한다.
어떻게 찾아야지 하다가 문자열 검색에 flag{를 해보았고 이런 tryUnlock이라는 함수를 보았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public final String tryUnlock(String blob, String phrase) {
String strDecrypt;
Intrinsics.checkNotNullParameter(blob, "blob");
Intrinsics.checkNotNullParameter(phrase, "phrase");
try {
strDecrypt = decrypt(blob, phrase);
} catch (Exception unused) {
}
if (StringsKt.startsWith$default(strDecrypt, "flag{", false, 2, (Object) null)) {
return strDecrypt;
}
return null;
}
decrypt한 값이 flag{로 시작하면 flag가 맞다는 뜻인듯 하다.
이 tryunlock은 BossRoundActivitykt..에서 호출이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public final Object invokeSuspend(Object obj) {
String strTryUnlock;
Object coroutine_suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
int i = this.label;
if (i == 0) {
ResultKt.throwOnFailure(obj);
this.label = 1;
obj = PipelineCodec.INSTANCE.fetchVaultBlob(this);
if (obj == coroutine_suspended) {
return coroutine_suspended;
}
} else {
if (i != 1) {
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
ResultKt.throwOnFailure(obj);
}
String str = (String) obj;
if (str != null) {
strTryUnlock = PipelineCodec.INSTANCE.tryUnlock(str, this.$typedPhrase);
} else {
strTryUnlock = null;
}
BossRoundActivityKt.BossRoundFlow$lambda$14(this.$unlockLoading$delegate, false);
if (strTryUnlock == null) {
BossRoundActivityKt.BossRoundFlow$lambda$11(this.$unlockError$delegate, "ACCESS DENIED");
} else {
this.$finalFlag$delegate.setValue(strTryUnlock);
BossRoundActivityKt.BossRoundFlow$lambda$2(this.$stage$delegate, BossStage.WIN);
}
return Unit.INSTANCE;
}
tryUnlock(str, this.$typedPhrase); 부분이고, 인자로는 str과 this.$typedPhrase가 들어간다.
우선 str은
1
2
obj = PipelineCodec.INSTANCE.fetchVaultBlob(this);
String str = (String) obj;
에서 오는 값이고, fetchVaultBlob은
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static final class C06742 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super String>, Object> {
int label;
C06742(Continuation<? super C06742> continuation) {
super(2, continuation);
}
@Override // kotlin.coroutines.jvm.internal.BaseContinuationImpl
public final Continuation<Unit> create(Object obj, Continuation<?> continuation) {
return new C06742(continuation);
}
@Override // kotlin.jvm.functions.Function2
public final Object invoke(CoroutineScope coroutineScope, Continuation<? super String> continuation) {
return ((C06742) create(coroutineScope, continuation)).invokeSuspend(Unit.INSTANCE);
}
@Override // kotlin.coroutines.jvm.internal.BaseContinuationImpl
public final Object invokeSuspend(Object obj) {
IntrinsicsKt.getCOROUTINE_SUSPENDED();
if (this.label == 0) {
ResultKt.throwOnFailure(obj);
JSONObject jsonObject = PipelineCodec.INSTANCE.getJsonObject(PipelineCodec.INSTANCE.nodeUrl(PipelineCodec.INSTANCE.getNodeVault()));
if (jsonObject != null && jsonObject.has("blob")) {
return jsonObject.optString("blob");
}
return null;
}
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
public final Object fetchVaultBlob(Continuation<? super String> continuation) {
return BuildersKt.withContext(Dispatchers.getIO(), new C06742(null), continuation);
}
C06742코드이다.
1
2
JSONObject jsonObject = PipelineCodec.INSTANCE.getJsonObject(PipelineCodec.INSTANCE.nodeUrl
(PipelineCodec.INSTANCE.getNodeConfig()));
를 뜯어보면
1
2
3
4
private static final String ENC_BASE = "aHR0cHM6Ly9tb2JpbGVoYWNraW5na3ItZGVmYXVsdC1ydGRiLmZpcmViYXNlaW8uY29t";
private static final String ENC_NODE_A = "Y29uZmln";
private static final String ENC_NODE_B = "dmx0XzllMWI0MA==";
private static final String ENC_SALT = "d2dwLnJ0ZGIubm9kZS52MQ==";
각각
decodeB64(ENC_NODE_A);= config
decodeB64(ENC_NODE_B);= vlt_9e1b40
decodeB64(ENC_SQLT);= wgp.rtdb.node.v1
decodeB64(ENC_BASE);= https://mobilehackingkr-default-rtdb.firebaseio.com
이고, 합치면 getJsonObject(https://mobilehackingkr-default-rtdb.firebaseio.com/vlt_9e1b40.json)가 된다.
사이트에 접속해보면 {"blob":"bqhnS5RWnuWEdQM2TLRNYa5MmAofpU6lN2L+R//KgHDUaIc="}라는 값이 있고 이것이 blob이다.
그런데 코드를 조금 더 보니까 https://mobilehackingkr-default-rtdb.firebaseio.com/config.json 이라는 부분도 있다.
들어가보니
1
2
3
4
{"app_version":"1.0","build":"7f3a2c","leaderboard":[{"name":"LUIGI","score":42000},
{"name":"PEACH","score":38000},{"name":"TOAD","score":31000}],"notice":"Player analytics moved.
Per-build ops console lives at node ops_diag_<build> (read-restricted, not indexed at root).
Contact ops@marioworld.dev"}
요런 값이 있고 ops_diag_build를 보라길래
https://mobilehackingkr-default-rtdb.firebaseio.com/ops_diag_7f3a2c.json을 보았더니
{"note":"legacy diagnostics console","pass_phrase":"w4rp_z0ne_1985","vault_ref":"vlt_9e1b40"}
이렇게 pass_phrase값인 w4rp_z0ne_1985를 찾았다.
flag{1ntent_p1p3_to_f1r3b4s3_vau1t}를 획득했다.