Shi0shishi0

汐鹿生

InterKosenCTF - Writeup

はじめに

高専に縁があるわけではありませんが, 2019/1/18 - 2019/1/20に開催されたInterKosenCTFに参加させていただきました.

自分は4問解いて450点入れました. あまり参加出来ず, すみませんでした.

Forensics 50 attack log

pcapファイルが与えられる. Basic認証のログイン試行を何度も行っているっぽい.

殆どのパケットに対する応答が401 Unauthorizedになっているので, この中にHTTPステータスが 200 OK なパケットが混じってそう.

適当にGrepするとログインに成功しているパケットが見つかった.

f:id:echoha610:20190123002810p:plain

(2019年だから2019番目のStream?)

The flag is KOSENCTF{<the password for the basic auth>} とあるので, Authorization: Basicヘッダにセットされている値 a29zZW46YlJ1dDNGMHJjM1cwcmszRA==Base64デコードしてパスワードを確認する.

デコードして得られたパスワード bRut3F0rc3W0rk3D がFLAG.

KOSENCTF{bRut3F0rc3W0rk3D}

Forensics 200 conversation

android_8.1_x86_oreo.imgという, Androidのイメージファイルっぽいものが与えられる. 問題文より, スマートフォンのイメージ?

イメージファイルをAutopsyで開く. メッセージが11件確認できる.

f:id:echoha610:20190124004732p:plain

メッセージのやりとりにMake our conversation secret by using our app, ok?Sure. というものがある. 何らかのアプリケーションで会話を暗号化しようとしている?(2018-12-27 13:10:01 JST)

その直後のやり取りがpwgh/nXO1tMf6TXUd99mhNH01GcCqVDxDBy1+sDf37s4nnYRuHkS+AOoiH3DmKU3I+ZYHEsllcwlnm6FWjAb5g== (2018-12-27 13:11:54 JST) と JSTGVuIBG/lSSUNW6jZqR20hw== (2018-12-27 13:12:29) になっている.

何らかの暗号化 + Base64エンコードが書けられたようなメッセージが確認できたので, 恐らくこの中にFLAGが含まれている.

次は暗号化を行っていると思われるアプリケーションを探す.

f:id:echoha610:20190124004318p:plain

アプリケーションがある場所といえばappディレクトリ?と思ってappディレクトリを確認すると, com.kosenctf.kosencrypto-DQEyRCoLoNfHq4_wVFgoPA== という怪しげなディレクトリがある.

その中にはbase.apkというapkファイルが入っている. このapkファイルは怪しそうなので解析していく.

dex2jarでdexファイルをjarファイルに変換後、Jadデコンパイルを行なった.

デコンパイルして得られたコードのMainActivityは以下の通り. AES暗号のCBCモードでデータを暗号化して, Base64エンコードしていると思われる.

package com.kosenctf.kosencrypto;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Base64;
import java.util.Base64.Encoder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class MainActivity
  extends Activity
{
  private String algorithm = "AES/CBC/PKCS5Padding";
  private String iv = "str0ng-s3cr3t-1v";
  private String key = "p4ssw0rd-t0-hid3";
  
  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2131296284);
    ((Button)findViewById(2131165218)).setOnClickListener(new View.OnClickListener()
    {
      public void onClick(View paramAnonymousView)
      {
        String str = ((EditText)MainActivity.this.findViewById(2131165277)).getText().toString();
        try
        {
          paramAnonymousView = Cipher.getInstance(MainActivity.this.algorithm);
          Object localObject1 = new javax/crypto/spec/SecretKeySpec;
          ((SecretKeySpec)localObject1).<init>(MainActivity.this.key.getBytes(), MainActivity.this.algorithm);
          Object localObject2 = new javax/crypto/spec/IvParameterSpec;
          ((IvParameterSpec)localObject2).<init>(MainActivity.this.iv.getBytes());
          paramAnonymousView.init(1, (Key)localObject1, (AlgorithmParameterSpec)localObject2);
          localObject2 = (EditText)MainActivity.this.findViewById(2131165226);
          localObject1 = new java/lang/String;
          ((String)localObject1).<init>(Base64.getEncoder().encode(paramAnonymousView.doFinal(str.getBytes())));
          ((EditText)localObject2).setText((CharSequence)localObject1);
        }
        catch (Exception paramAnonymousView)
        {
          ((EditText)MainActivity.this.findViewById(2131165226)).setText("Failed in encrypting the plaintext.");
        }
      }
    });
  }
}

CyberChefに暗号化されていたメッセージを与えて, AES暗号で復号する. その後, 復号して得られたデータをそのままBase64デコードする. (鍵とIVはハードコードされているものを使用)

1つ目のメッセージの復号結果.

https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true)AES_Decrypt(%7B'option':'Base64','string':'cDRzc3cwcmQtdDAtaGlkMw%3D%3D'%7D,%7B'option':'Base64','string':'c3RyMG5nLXMzY3IzdC0xdg%3D%3D'%7D,'CBC','Raw','Raw',%7B'option':'Hex','string':''%7D)&input=cHdnaC9uWE8xdE1mNlRYVWQ5OW1oTkgwMUdjQ3FWRHhEQnkxK3NEZjM3czRubllSdUhrUytBT29pSDNEbUtVM0krWllIRXNsbGN3bG5tNkZXakFiNWc9PQk

2つ目のメッセージの復号結果.

https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true)AES_Decrypt(%7B'option':'Base64','string':'cDRzc3cwcmQtdDAtaGlkMw%3D%3D'%7D,%7B'option':'Base64','string':'c3RyMG5nLXMzY3IzdC0xdg%3D%3D'%7D,'CBC','Raw','Raw',%7B'option':'Hex','string':''%7D)&input=R1Z1SUJHL2xTU1VOVzZqWnFSMjBodz09

The flag is KOSENCTF{7h3_4r7_0f_4ndr01d_f0r3n51c5}I got it. という文章が得られた.

KOSENCTF{7h3_4r7_0f_4ndr01d_f0r3n51c5}

Reversing 100 flag generator

x86-64のELFファイルが与えられる. ファイル名は main.

プログラムは最初に怪しげなデータを変数に格納し, time関数を実行する. この関数の実行結果をr関数に渡して, 関数内で計算処理を行う.

行なっている計算は (time関数の実行結果 * 0x41C64E6D + 0x3039) & 0x7FFFFFFF となっている.

f:id:echoha610:20190124010925p:plain

その次に, アドレス0x401220 の cmp [rbp+var_38], eax でr関数の計算結果(eax)と[rbp+var_38]を比較している.

f:id:echoha610:20190124011034p:plain

eaxの値と[rbp+var_38]の値が一致する場合, 暗号化されているデータをxor処理してprintf関数で出力すると思われる処理に入る. そうでない場合, Sleep関数実行後にtime関数の前の処理まで戻る.

プログラムを普通に叩くと期待される値がr関数に与えられない限り, ひたすら計算処理とSleepを繰り返すことになるはず.

デバッガでプログラムを動作させて確認すると, [rbp+var_38] には 0x25DC167E が入っている事が分かる.

以上より, (x * 0x41C64E6D + 0x3039) & 0x7FFFFFFF == 0x25DC167E となるような値xを求め, time関数の実行結果をその値で上書きすれば良いはず.

z3で簡単に求められそうなのでスクリプトを書く. 求める値のサイズはtime関数が返す値と同じサイズの4バイト.

from z3 import *

p, q = Bools(["p", "q"])
x = BitVec('x', 32)

s = Solver()
s.add((x * 0x41C64E6D + 0x3039) & 0x7FFFFFFF == 0x25DC167E)

r = s.check()
if r == sat:
    m = s.model()

number = m[x].as_long()

print(number)

上記のスクリプトより, 1509961785 = 0x5A003039 という値が求められる.

プログラムを実行し, time関数実行直後にeaxレジスタの値を 0x5A003039 に書き換えてプログラムを実行する. 期待される値が関数rに与えられたので処理は復号ルーチンに進み, 複数回のループ処理後, FLAGが得られる.

※ 標準出力に出ていたFLAGは末尾が欠けていた. Submitしても何度か弾かれたため, デバッガを通してメモリ上の値を確認したら末尾に"?"があることが分かったので修正してSubmitした.

KOSENCTF{IS_THIS_REALLY_A_REVERSING?}

Cheat 100 lights out

.NET製のexeファイルが与えられる. 実行するとライツアウトっぽいゲームの画面が表示される.

dnspyでファイルを開いて上から眺めていくと, 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>> にデータを格納してそれをxor処理している箇所がある.

     // Token: 0x06000017 RID: 23 RVA: 0x0000246C File Offset: 0x0000066C
        // Note: this type is marked as 'beforefieldinit'.
        static 745BBE96-A34C-4723-B7D4-089F48D57F2E()
        {
            745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>> = new byte[]
            {
                200,
                223,
                198,
                210,
                158,
                149,
                232,
                159,
                223,
                216,
                145,
                155,
                226,
                149,
                217,
                238,
                245,
                232,
                253,
                247,
                253,
                235,
                250,
                198,
                193,
                199,
                132,
                197,
                223,
                212,
                128,
                217,
                230,
                242,
                215,
                237,
                189,
                224,
                238,
                235,
                247,
                240,
                227,
                181,
                242,
                180,
                219,
                202,
                200,
                196,
                252,
                224,
                240,
                171,
                241,
                244,
                241,
                167,
                252,
                253,
                239,
                200,
                247,
                253,
                217,
                223,
                156,
                148,
                173,
                128,
                130,
                138,
                144,
                130,
                148,
                148,
                138,
                134,
                144,
                140,
                149,
                149,
                139,
                216,
                179,
                158,
                149,
                147,
                180,
                156,
                130,
                156,
                186,
                158,
                147,
                157,
                190,
                184,
                232,
                134,
                187,
                187
            };
            for (int i = 0; i < 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>.Length; i++)
            {
                745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[i] = (byte)((int)745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[i] ^ i ^ 170);
            }
        }

エンコードされたデータのデコード処理に見えるので, 処理の内容を手元で再現してみる.

import sys

list = [200, 223, 198, 210, 158, 149, 232, 159, 223, 216, 145, 155, 226, 149, 217, 238, 245, 232, 253, 247, 253, 235, 250, 198, 193, 199, 132, 197, 223, 212, 128, 217, 230, 242, 215, 237, 189, 224, 238, 235, 247, 240, 227, 181, 242, 180, 219, 202, 200, 196, 252, 224, 240, 171, 241, 244, 241, 167, 252, 253, 239, 200, 247, 253, 217, 223, 156, 148, 173, 128, 130, 138, 144, 130, 148, 148, 138, 134, 144, 140, 149, 149, 139, 216, 179, 158, 149, 147, 180, 156, 130, 156, 186, 158, 147, 157, 190, 184, 232, 134, 187, 187]

for i in range(len(list)):
    a = list[i]
    b = a ^ i ^ 170
    sys.stdout.write(chr(b))

上のスクリプトを実行すると以下のような文字列が得られた.

$ python solver.py
btn{0:D2}{1:D2}KOSENCTF{st4tic4lly_d3obfusc4t3_OR_dyn4mic4lly_ch34t}Congratulations!MainFormLights Out

745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(1, 15, 53) の部分がFLAGになっている.

     // Token: 0x06000012 RID: 18 RVA: 0x000023F8 File Offset: 0x000005F8
        public static string 햎()
        {
            return 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[0] ?? 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(0, 0, 15);
        }

        // Token: 0x06000013 RID: 19 RVA: 0x0000240E File Offset: 0x0000060E
        public static string 헡()
        {
            return 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[1] ?? 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(1, 15, 53);
        }

        // Token: 0x06000014 RID: 20 RVA: 0x00002425 File Offset: 0x00000625
        public static string 헋()
        {
            return 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[2] ?? 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(2, 68, 16);
        }

        // Token: 0x06000015 RID: 21 RVA: 0x0000243C File Offset: 0x0000063C
        public static string 햝()
        {
            return 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[3] ?? 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(3, 84, 8);
        }

        // Token: 0x06000016 RID: 22 RVA: 0x00002452 File Offset: 0x00000652
        public static string 햙()
        {
            return 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[4] ?? 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(4, 92, 10);
        }

多分動いているプログラムをダンプしてStringsコマンドに掛ける, とかでもFLAGを得られる気がする.

KOSENCTF{st4tic4lly_d3obfusc4t3_OR_dyn4mic4lly_ch34t}

おわりに

短い時間しか参加できませんでしたが面白かったです. 運営の方々, お疲れ様でした.

開催期間中に解けなかったFor300は解いた. Rev300もあとで復習したい.