InterKosenCTF - Writeup
はじめに
高専に縁があるわけではありませんが, 2019/1/18 - 2019/1/20に開催されたInterKosenCTFに参加させていただきました.
自分は4問解いて450点入れました. あまり参加出来ず, すみませんでした.
Forensics 50 attack log
pcapファイルが与えられる. Basic認証のログイン試行を何度も行っているっぽい.
殆どのパケットに対する応答が401 Unauthorized
になっているので, この中にHTTPステータスが 200 OK
なパケットが混じってそう.
適当にGrepするとログインに成功しているパケットが見つかった.
(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件確認できる.
メッセージのやりとりに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が含まれている.
次は暗号化を行っていると思われるアプリケーションを探す.
アプリケーションがある場所といえば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つ目のメッセージの復号結果.
2つ目のメッセージの復号結果.
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
となっている.
その次に, アドレス0x401220 の cmp [rbp+var_38], eax
でr関数の計算結果(eax)と[rbp+var_38]を比較している.
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もあとで復習したい.