../

STDiO 2023 Capture The Flag: 06 - Top Terrorist

A state sponsored hacker group named ‘Thailand Cyber Top Terrorist’ has its own way to verify group members before allowing them to enter the secret area. The group implemented its own mobile application and let members fill in the secret to verify themselves. Find a way to get the secret.

Download: https://stdio-2026-public.2600.in.th/topterrorist.apk

Author: Pgoodboy

โจทย์นี้เป็น Android Application Reverse Engineer ขั้นตอนแรก ดาวน์โหลดไฟล์ apk และ decompiler ก่อนเลยจะได้ดูโค้ดได้ ไปที่ https://www.decompiler.com/

อัพโหลด apk เมื่อ decompile เสร็จ ดาวน์โหลด Zip ลงมาเก็บไว้

เมื่อแตกไฟล์ออกมาจะได้ โฟลเดอร์ sources และ resources โดย sources ก็คือ java,kotlin โค้ดการทำงานของแอพฯ และ resources ก็เก็บไฟล์ต่างๆที่เกี่ยวของกับแอพฯ เช่น string label ,icon รูปภาพ และอีกมากมายต้องไปศึกษาการเขียน android แอพฯเพิ่มเติม โดย android แอพฯจะเริ่มจาก AndroidManifest.xml ซึ่งจะอยู่ที่ resources/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="32" android:compileSdkVersionCodename="12" package="com.stdio.thailandcybertopterrorist" platformBuildVersionCode="32" platformBuildVersionName="12">
    <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="32"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <application android:theme="@style/Theme.ThailandTopTerrorist" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:supportsRtl="true" android:extractNativeLibs="false" android:fullBackupContent="@xml/backup_rules" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules">
        <activity android:name="com.stdio.thailandcybertopterrorist.MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <meta-data android:name="android.app.lib_name" android:value=""/>
        </activity>
        <provider android:name="androidx.startup.InitializationProvider" android:exported="false" android:authorities="com.stdio.thailandcybertopterrorist.androidx-startup">
            <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
        </provider>
    </application>
</manifest>

โดยไฟล์นี้จะบอกว่าเมื่อแอพฯเริ่มต้นทำงานจะเริ่มจาก class ไหน ก็จะเห็นว่ามันเริ่มที่ class com.stdio.thailandcybertopterrorist.MainActivity

<activity android:name="com.stdio.thailandcybertopterrorist.MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <meta-data android:name="android.app.lib_name" android:value=""/>
</activity>

เราก็จะไปดูโค้ดกันที่นี่ sources/com/stdio/thailandcybertopterrorist/MainActivity.java

package com.stdio.thailandcybertopterrorist;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Arrays;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import kotlin.jvm.internal.Ref;
import kotlin.jvm.internal.StringCompanionObject;
import kotlin.ranges.IntProgression;
import kotlin.ranges.RangesKt;
import kotlin.text.StringsKt;
import okhttp3.CertificatePinner;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.Retrofit;

@Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0014¨\u0006\u0007"}, d2 = {"Lcom/stdio/thailandcybertopterrorist/MainActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app_release"}, k = 1, mv = {1, 7, 1}, xi = 48)
/*compiled from: MainActivity.kt */
public final class MainActivity extends AppCompatActivity {
    /* access modifiers changed from: protected*/
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) R.layout.activity_main);
        Ref.ObjectRef objectRef = new Ref.ObjectRef();
        T findViewById = findViewById(R.id.editTextTextPassword);
        Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(R.id.editTextTextPassword)");
        objectRef.element = findViewById;
        Intrinsics.checkNotNullExpressionValue("0123456789abcdef".toCharArray(), "this as java.lang.String).toCharArray()");
        CertificatePinner build = new CertificatePinner.Builder().add(getString(R.string.hackerURL), "sha256/J2/oqMTsdhFWW/n85tys6b4yDBtb6idZayIEBx7QTxA=").build();
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.addInterceptor(new MainActivity$$ExternalSyntheticLambda0());
        Retrofit build2 = new Retrofit.Builder().baseUrl("https://" + getString(R.string.hackerURL)).client(builder.certificatePinner(build).build()).build();
        Intrinsics.checkNotNullExpressionValue(build2, "Builder()\n            .b…ent)\n            .build()");
        View findViewById2 = findViewById(R.id.Verify);
        Intrinsics.checkNotNull(findViewById2, "null cannot be cast to non-null type android.widget.Button");
        ((Button) findViewById2).setOnClickListener(new MainActivity$$ExternalSyntheticLambda1((Apicall) build2.create(Apicall.class), this, objectRef));
    }
...
...
}

เมื่อดูใน method onCreate จะเห็น

...
CertificatePinner build = new CertificatePinner.Builder().add(getString(R.string.hackerURL), "sha256/J2/oqMTsdhFWW/n85tys6b4yDBtb6idZayIEBx7QTxA=").build();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
...

มันสร้าง http client ต้องไปที่ไหนสักที่แน่ๆ getString(R.string.hackerURL) ตามไปดูกัน R.string.hackerURL ก็คือ String resource โดยมันจะเก็บอยู่ที่ resources/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string name="hackerURL">topterrorist-stdio.2600.in.th</string>
...
</resources>

รู้ปลายทางแล้วลองเข้าไปดู

curl -L topterrorist-stdio.2600.in.th
You are not a Thailand Cyber Top Terrorist 8==D

อิหยังเนี่ย ก็ไล่โค้ดต่อ

   /*access modifiers changed from: private*/
    public static final Response onCreate$lambda$2$lambda$1(Interceptor.Chain chain) {
        Request.Builder newBuilder = chain.request().newBuilder();
        newBuilder.header("User-Agent", "stdio2077");
        return chain.proceed(newBuilder.build());
    }

เห็น newBuilder.header(“User-Agent”, “stdio2077”); น่าสนใจ

curl -A "stdio2077" -L topterrorist-stdio.2600.in.th
OHrQNeAcpDuv2F8dtttUfJ3IyILa3AjJFWs4i2ScxX034UOxQRzPYWfBFYSWv5rfaECBgNCy0Xoi0rs=

ก็ยังไม่เข้าใจก็ไปกันต่อ เข้าไปดูโค้ดในไฟล์

sources/com/stdio/thailandcybertopterrorist/MainActivity$onCreate$1$1.java

public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        String str;
        EditText editText;
        Intrinsics.checkNotNullParameter(call, NotificationCompat.CATEGORY_CALL);
        Intrinsics.checkNotNullParameter(response, "response");
        ResponseBody body = response.body();
        if (body != null) {
            str = body.string();
            Intrinsics.checkNotNullExpressionValue(str, "it.string()");
        } else {
            str = "";
        }
        String string = this.this$0.getString(R.string.aesKey);
        Intrinsics.checkNotNullExpressionValue(string, "getString(R.string.aesKey)");
        String string2 = this.this$0.getString(R.string.iv);
        Intrinsics.checkNotNullExpressionValue(string2, "getString(R.string.iv)");
        Cipher instance = Cipher.getInstance("AES/GCM/NoPadding");
        byte[] bytes = string2.getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
        byte[] bytes2 = string.getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)");
        instance.init(2, MainActivity.onCreate$getSecretKeyFromString(bytes2), new GCMParameterSpec(128, bytes));
        byte[] doFinal = instance.doFinal(Base64.getDecoder().decode(str));
        View findViewById = this.this$0.findViewById(R.id.textView);
        Intrinsics.checkNotNull(findViewById, "null cannot be cast to non-null type android.widget.TextView");
        TextView textView = (TextView) findViewById;
        System.out.println("textInput");
        Intrinsics.checkNotNullExpressionValue(doFinal, "decryptedStr");
        String str2 = new String(doFinal, Charsets.UTF_8);
        if (this.$textInput.element == null) {
            Intrinsics.throwUninitializedPropertyAccessException("textInput");
            editText = null;
        } else {
            editText = (EditText) this.$textInput.element;
        }
        if (str2.equals(editText.getText().toString())) {
            System.out.println("text match");
            textView.setText("Congratulations, you are a hacker !");
            return;
        }
        textView.setText("You are not a hacker 8==D");
    }

method นี้ดูเหมือนจะเป็นการเช็คอะไรสักอย่าง ไล่ดู string resource ใน resources/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string name="aesKey">5258029f3f7c38088a9295941b7d0c78</string>
<string name="iv">g3bkm9f81r3cywcjxpzw3206jzh0b9zw</string>
...
</resources>

คิดว่าใช่ล่ะ ทำการสร้าง java แอพฯ โดยตัดส่วนที่เป็น android ออกไป

mvn archetype:generate -DgroupId=com.crackstdio.app -DartifactId=crackstdio -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:3.2.0:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:3.2.0:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO]
[INFO] --- maven-archetype-plugin:3.2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.crackstdio.app
[INFO] Parameter: artifactId, Value: crackstdio
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.crackstdio.app
[INFO] Parameter: packageInPathFormat, Value: com/crackstdio/app
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.crackstdio.app
[INFO] Parameter: groupId, Value: com.crackstdio.app
[INFO] Parameter: artifactId, Value: crackstdio
[INFO] Project created from Archetype in dir: /home/naidherng/Playground/java/crackstdio
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  30.381 s
[INFO] Finished at: 2023-12-03T21:48:05+07:00
[INFO] ------------------------------------------------------------------------

cd crackstdio

เพิ่ม

    <dependency>
     <groupId>org.jetbrains.kotlin</groupId>
     <artifactId>kotlin-stdlib</artifactId>
     <version>1.3.21</version>
    </dependency>

ใน pom.xml

เขียนโค้ดโดยปรับจาก MainActivity class และ MainActivity$onCreate$1$1 class โดยเอา response จาก curl มาตรวจสอบ

crackstdio/src/main/java/com/crackstdio/app/App.java

package com.crackstdio.app;

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import kotlin.text.Charsets;

public class App
{
    public static void main( String[] args )
    {
      try{
        System.out.println( "Hello Flag!" );

        /* response จาก curl -A "stdio2077" -L topterrorist-stdio.2600.in.th */
        String str = "OHrQNeAcpDuv2F8dtttUfJ3IyILa3AjJFWs4i2ScxX034UOxQRzPYWfBFYSWv5rfaECBgNCy0Xoi0rs=";

        /* <string name="aesKey">5258029f3f7c38088a9295941b7d0c78</string> */
        String string = "5258029f3f7c38088a9295941b7d0c78";

        /* <string name="iv">g3bkm9f81r3cywcjxpzw3206jzh0b9zw</string> */
        String string2 = "g3bkm9f81r3cywcjxpzw3206jzh0b9zw";

        Cipher instance = Cipher.getInstance("AES/GCM/NoPadding");
        byte[] bytes = string2.getBytes(Charsets.UTF_8);
        byte[] bytes2 = string.getBytes(Charsets.UTF_8);
        instance.init(2, new SecretKeySpec(bytes2, 0, bytes2.length, "AES"), new GCMParameterSpec(128, bytes));
        byte[] doFinal = instance.doFinal(Base64.getDecoder().decode(str));
        String str2 = new String(doFinal, Charsets.UTF_8);
        System.out.println(str2);
       }catch(Exception e){
          System.out.println(e.toString());
       }
    }
}
mvn package

ดาวน์โหลด kotlin-stdlib jar ไฟล์

รันเลยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยยย

java -classpath annotations-13.0.jar:kotlin-stdlib-1.9.21.jar:target/crackstdio-1.0-SNAPSHOT.jar com.crackstdio.app.App
Hello Flag!
STDIO23_6{63a6cb392983afc9e9bd8ec47ea8c381}

/stdio/ /ctf/ /Top Terrorist/ /Reverse/