Home (mobilehacking.kr) MarioWorld
Post
Cancel

(mobilehacking.kr) MarioWorld

이번에는 게임 컨셉의 문제다.

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}를 획득했다.

This post is written by PRO.